From 2d7b96821cf48330314da9e763b26a6d48a04926 Mon Sep 17 00:00:00 2001 From: Radhika Rao Date: Mon, 13 Jul 2026 16:49:02 +0530 Subject: [PATCH] gradient: add doctl commands for Anthropic API Keys (APICLI-2960) Implements the Anthropic API Keys operations under `doctl gradient anthropic-key`, mirroring the existing OpenAI keys commands: - list, create, get, update, delete - get-agents (list agents using an Anthropic key) Adds the service-layer methods, displayer, generated mock, and unit + integration tests. Co-authored-by: Cursor --- args.go | 6 + commands/displayers/gradientai.go | 50 ++++ commands/gradientai.go | 2 + commands/gradientai_anthropic_key.go | 211 ++++++++++++++++ commands/gradientai_anthropic_key_test.go | 99 ++++++++ do/gradientai.go | 109 ++++++++ do/mocks/GradientAIService.go | 90 +++++++ .../gradientai_anthropic_key_create_test.go | 102 ++++++++ .../gradientai_anthropic_key_delete_test.go | 103 ++++++++ .../gradientai_anthropic_key_get_test.go | 233 ++++++++++++++++++ .../gradientai_anthropic_key_list_test.go | 85 +++++++ .../gradientai_anthropic_key_update_test.go | 103 ++++++++ 12 files changed, 1193 insertions(+) create mode 100644 commands/gradientai_anthropic_key.go create mode 100644 commands/gradientai_anthropic_key_test.go create mode 100644 integration/gradientai_anthropic_key_create_test.go create mode 100644 integration/gradientai_anthropic_key_delete_test.go create mode 100644 integration/gradientai_anthropic_key_get_test.go create mode 100644 integration/gradientai_anthropic_key_list_test.go create mode 100644 integration/gradientai_anthropic_key_update_test.go diff --git a/args.go b/args.go index 7951038a1..501ccbd6d 100644 --- a/args.go +++ b/args.go @@ -890,6 +890,12 @@ const ( // ArgOpenAIKeyAPIKey is the API key for the OpenAI API Key ArgOpenAIKeyAPIKey = "api-key" + // ArgAnthropicKeyName is the name of the Anthropic API Key + ArgAnthropicKeyName = "name" + + // ArgAnthropicKeyAPIKey is the API key for the Anthropic API Key + ArgAnthropicKeyAPIKey = "api-key" + // Dedicated Inference Args // ArgDedicatedInferenceSpec is the path to a dedicated inference spec file. diff --git a/commands/displayers/gradientai.go b/commands/displayers/gradientai.go index 36449be7f..d734d9f28 100644 --- a/commands/displayers/gradientai.go +++ b/commands/displayers/gradientai.go @@ -614,6 +614,56 @@ func (o *OpenAiApiKey) KV() []map[string]any { return out } +type AnthropicApiKey struct { + AnthropicApiKeys do.AnthropicApiKeys +} + +var _ Displayable = &AnthropicApiKey{} + +func (a *AnthropicApiKey) JSON(out io.Writer) error { + return writeJSON(a.AnthropicApiKeys, out) +} + +func (a *AnthropicApiKey) Cols() []string { + return []string{ + "Name", + "UUID", + "CreatedAt", + "CreatedBy", + "UpdatedAt", + "DeletedAt", + } +} + +func (a *AnthropicApiKey) ColMap() map[string]string { + return map[string]string{ + "Name": "Name", + "UUID": "UUID", + "CreatedAt": "Created At", + "CreatedBy": "Created By", + "UpdatedAt": "Updated At", + "DeletedAt": "Deleted At", + } +} + +func (a *AnthropicApiKey) KV() []map[string]any { + if a == nil || a.AnthropicApiKeys == nil { + return []map[string]any{} + } + out := make([]map[string]any, 0, len(a.AnthropicApiKeys)) + for _, key := range a.AnthropicApiKeys { + out = append(out, map[string]any{ + "Name": key.Name, + "UUID": key.Uuid, + "CreatedAt": key.CreatedAt, + "CreatedBy": key.CreatedBy, + "UpdatedAt": key.UpdatedAt, + "DeletedAt": key.DeletedAt, + }) + } + return out +} + type Model struct { Models []do.Model } diff --git a/commands/gradientai.go b/commands/gradientai.go index e9f5e3b95..79209c92b 100644 --- a/commands/gradientai.go +++ b/commands/gradientai.go @@ -37,6 +37,8 @@ func GradientAI() *Command { cmd.AddCommand(ListRegionsCmd()) // Add the OpenAI keys command as a subcommand to Gradient AI cmd.AddCommand(OpenAIKeyCmd()) + // Add the Anthropic keys command as a subcommand to Gradient AI + cmd.AddCommand(AnthropicKeyCmd()) return cmd } diff --git a/commands/gradientai_anthropic_key.go b/commands/gradientai_anthropic_key.go new file mode 100644 index 000000000..4d8864dac --- /dev/null +++ b/commands/gradientai_anthropic_key.go @@ -0,0 +1,211 @@ +package commands + +import ( + "fmt" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/commands/displayers" + "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" + "github.com/spf13/cobra" +) + +func AnthropicKeyCmd() *Command { + cmd := &Command{ + Command: &cobra.Command{ + Use: "anthropic-key", + Aliases: []string{"ak"}, + Short: "Display commands that manage DigitalOcean Anthropic API Keys.", + Long: "The subcommands of `doctl gradient anthropic-key` allow you to access and manage Anthropic API keys.", + }, + } + + cmdAnthropicKeyList := CmdBuilder( + cmd, + RunAnthropicKeyList, + "list", + "Lists all Anthropic API Keys", + "Lists all Anthropic API Keys available in your account", + Writer, aliasOpt("ls"), + displayerType(&displayers.AnthropicApiKey{}), + ) + cmdAnthropicKeyList.Example = `The following example lists information about all Anthropic API Keys ` + "\n" + + ` doctl gradient anthropic-key list ` + + cmdAnthropicKeyGet := CmdBuilder( + cmd, + RunAnthropicKeyGet, + "get ", + "Retrieves an Anthropic API Key by its UUID", + "Retrieves information about an Anthropic API Key", + Writer, aliasOpt("g"), + displayerType(&displayers.AnthropicApiKey{}), + ) + cmdAnthropicKeyGet.Example = `The following example retrieves information about an Anthropic API Key with ID - f81d4fae-0000-11d0-a765-000000000000` + "\n" + + ` doctl gradient anthropic-key get f81d4fae-0000-11d0-a765-000000000000` + + cmdAnthropicKeyGetAgents := CmdBuilder( + cmd, + RunAnthropicKeyGetAgents, + "get-agents ", + "Lists agents using an Anthropic API Key", + "Lists all agents that are using the specified Anthropic API Key", + Writer, aliasOpt("ga"), + displayerType(&displayers.Agent{}), + ) + cmdAnthropicKeyGetAgents.Example = `The following example retrieves information about an Anthropic API Key with ID - f81d4fae-0000-11d0-a765-000000000000` + "\n" + + ` doctl gradient anthropic-key get-agents f81d4fae-0000-11d0-a765-000000000000 ` + + cmdAnthropicKeyCreate := CmdBuilder( + cmd, + RunAnthropicKeyCreate, + "create", + "Creates an Anthropic API Key", + "Creates a new Anthropic API Key with the specified name and API key.", + Writer, aliasOpt("c"), + displayerType(&displayers.AnthropicApiKey{}), + ) + cmdAnthropicKeyCreate.Example = `The following example creates an Anthropic API Key ` + "\n" + + ` doctl gradient anthropic-key create --name my-key --api-key sk-ant-1234567890abcdef1234567890abcdef ` + AddStringFlag(cmdAnthropicKeyCreate, "name", "", "", "The name of the Anthropic API Key.", requiredOpt()) + AddStringFlag(cmdAnthropicKeyCreate, "api-key", "", "", "The API key for the Anthropic API Key.", requiredOpt()) + + cmdAnthropicKeyUpdate := CmdBuilder( + cmd, + RunAnthropicKeyUpdate, + "update ", + "Updates an Anthropic API Key by its UUID", + "Updates an existing Anthropic API Key with the specified name and API key.", + Writer, aliasOpt("u"), + displayerType(&displayers.AnthropicApiKey{}), + ) + cmdAnthropicKeyUpdate.Example = `The following example updates an Anthropic API Key with ID - f81d4fae-0000-11d0-a765-000000000000 ` + "\n" + + ` doctl gradient anthropic-key update f81d4fae-0000-11d0-a765-000000000000 --name my-key --api-key sk-ant-1234567890abcdef1234567890abcdef ` + AddStringFlag(cmdAnthropicKeyUpdate, "name", "", "", "The name of the Anthropic API Key.") + AddStringFlag(cmdAnthropicKeyUpdate, "api-key", "", "", "The API key for the Anthropic API Key.") + + cmdAnthropicKeyDelete := CmdBuilder( + cmd, + RunAnthropicKeyDelete, + "delete ", + "Deletes an Anthropic API Key by its UUID", + "Deletes an Anthropic API Key by its UUID.", + Writer, aliasOpt("rm"), + ) + cmdAnthropicKeyDelete.Example = `The following example deletes an Anthropic API Key with ID - f81d4fae-0000-11d0-a765-000000000000 ` + "\n" + + ` doctl gradient anthropic-key delete f81d4fae-0000-11d0-a765-000000000000 ` + "\n" + + `Note - Anthropic Keys linked to DO Agents cannot be deleted unless you change it from agent` + AddBoolFlag(cmdAnthropicKeyDelete, doctl.ArgForce, "f", false, "Forces deletion without confirmation.") + + return cmd +} + +func RunAnthropicKeyList(c *CmdConfig) error { + anthropicApiKeys, err := c.GradientAI().ListAnthropicAPIKeys() + if err != nil { + return err + } + return c.Display(&displayers.AnthropicApiKey{AnthropicApiKeys: anthropicApiKeys}) +} + +func RunAnthropicKeyGet(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + + anthropicApiKey, err := c.GradientAI().GetAnthropicAPIKey(c.Args[0]) + if err != nil { + return err + } + return c.Display(&displayers.AnthropicApiKey{AnthropicApiKeys: do.AnthropicApiKeys{*anthropicApiKey}}) +} + +func RunAnthropicKeyGetAgents(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + + anthropicApiKeyID := c.Args[0] + agents, err := c.GradientAI().ListAgentsByAnthropicAPIKey(anthropicApiKeyID) + if err != nil { + return err + } + return c.Display(&displayers.Agent{Agents: agents}) +} + +func RunAnthropicKeyCreate(c *CmdConfig) error { + name, err := c.Doit.GetString(c.NS, doctl.ArgAnthropicKeyName) + if err != nil { + return err + } + + apiKey, err := c.Doit.GetString(c.NS, doctl.ArgAnthropicKeyAPIKey) + if err != nil { + return err + } + + anthropicApiKeyCreate := &godo.AnthropicAPIKeyCreateRequest{ + Name: name, + ApiKey: apiKey, + } + + anthropicApiKey, err := c.GradientAI().CreateAnthropicAPIKey(anthropicApiKeyCreate) + if err != nil { + return err + } + + return c.Display(&displayers.AnthropicApiKey{AnthropicApiKeys: do.AnthropicApiKeys{*anthropicApiKey}}) +} + +func RunAnthropicKeyUpdate(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + + anthropicApiKeyID := c.Args[0] + + name, err := c.Doit.GetString(c.NS, doctl.ArgAnthropicKeyName) + if err != nil { + return err + } + + apiKey, err := c.Doit.GetString(c.NS, doctl.ArgAnthropicKeyAPIKey) + if err != nil { + return err + } + + anthropicApiKeyUpdate := &godo.AnthropicAPIKeyUpdateRequest{ + Name: name, + ApiKey: apiKey, + ApiKeyUuid: anthropicApiKeyID, + } + + anthropicApiKey, err := c.GradientAI().UpdateAnthropicAPIKey(anthropicApiKeyID, anthropicApiKeyUpdate) + if err != nil { + return err + } + + return c.Display(&displayers.AnthropicApiKey{AnthropicApiKeys: do.AnthropicApiKeys{*anthropicApiKey}}) +} + +func RunAnthropicKeyDelete(c *CmdConfig) error { + if len(c.Args) < 1 { + return doctl.NewMissingArgsErr(c.NS) + } + + anthropicApiKeyID := c.Args[0] + force, err := c.Doit.GetBool(c.NS, doctl.ArgForce) + if err != nil { + return err + } + if force || AskForConfirmDelete("Anthropic API Key", 1) == nil { + _, err := c.GradientAI().DeleteAnthropicAPIKey(anthropicApiKeyID) + if err != nil { + return err + } + notice("Anthropic API Key deleted successfully") + } else { + return fmt.Errorf("operation aborted") + } + return nil +} diff --git a/commands/gradientai_anthropic_key_test.go b/commands/gradientai_anthropic_key_test.go new file mode 100644 index 000000000..e8a145c4c --- /dev/null +++ b/commands/gradientai_anthropic_key_test.go @@ -0,0 +1,99 @@ +package commands + +import ( + "testing" + + "github.com/digitalocean/doctl" + "github.com/digitalocean/doctl/do" + "github.com/digitalocean/godo" + "github.com/stretchr/testify/assert" +) + +var ( + testAnthropicKey = do.AnthropicApiKey{ + AnthropicApiKeyInfo: &godo.AnthropicApiKeyInfo{ + Uuid: "d35e5cb7-7957-4643-8e3a-1ab4eb3a494c", + Name: "Test Anthropic Key", + }, + } +) + +func TestAnthropicKeyCommand(t *testing.T) { + cmd := AnthropicKeyCmd() + assert.NotNil(t, cmd) + assertCommandNames(t, cmd, "create", "delete", "get", "get-agents", "list", "update") +} + +func TestAnthropicKeyGet(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + anthropic_key_id := "00000000-0000-4000-8000-000000000000" + config.Args = append(config.Args, anthropic_key_id) + tm.gradientAI.EXPECT().GetAnthropicAPIKey("00000000-0000-4000-8000-000000000000").Return(&testAnthropicKey, nil) + err := RunAnthropicKeyGet(config) + assert.NoError(t, err) + }) +} + +func TestAnthropicKeyList(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + tm.gradientAI.EXPECT().ListAnthropicAPIKeys().Return(do.AnthropicApiKeys{testAnthropicKey}, nil) + err := RunAnthropicKeyList(config) + assert.NoError(t, err) + }) +} + +func TestAnthropicKeyCreate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + + config.Doit.Set(config.NS, doctl.ArgAnthropicKeyName, "Test Anthropic Key") + config.Doit.Set(config.NS, doctl.ArgAnthropicKeyAPIKey, "sk-ant-proddfsefac") + + tm.gradientAI.EXPECT().CreateAnthropicAPIKey(&godo.AnthropicAPIKeyCreateRequest{ + Name: "Test Anthropic Key", + ApiKey: "sk-ant-proddfsefac", + }).Return(&testAnthropicKey, nil) + + err := RunAnthropicKeyCreate(config) + assert.NoError(t, err) + }) +} + +func TestAnthropicKeyDelete(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + anthropic_api_id := "00000000-0000-4000-8000-000000000000" + config.Args = append(config.Args, anthropic_api_id) + config.Doit.Set(config.NS, doctl.ArgForce, true) + tm.gradientAI.EXPECT().DeleteAnthropicAPIKey("00000000-0000-4000-8000-000000000000").Return(&testAnthropicKey, nil) + err := RunAnthropicKeyDelete(config) + assert.NoError(t, err) + }) +} + +func TestAnthropicKeyUpdate(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + anthropic_api_id := "00000000-0000-4000-8000-000000000000" + config.Args = append(config.Args, anthropic_api_id) + + config.Doit.Set(config.NS, doctl.ArgAnthropicKeyName, "Updated Anthropic Key") + config.Doit.Set(config.NS, doctl.ArgAnthropicKeyAPIKey, "updated-api-key") + + tm.gradientAI.EXPECT().UpdateAnthropicAPIKey("00000000-0000-4000-8000-000000000000", &godo.AnthropicAPIKeyUpdateRequest{ + Name: "Updated Anthropic Key", + ApiKey: "updated-api-key", + ApiKeyUuid: "00000000-0000-4000-8000-000000000000", + }).Return(&testAnthropicKey, nil) + + err := RunAnthropicKeyUpdate(config) + assert.NoError(t, err) + }) +} + +func TestAnthropicKeyGetAgents(t *testing.T) { + withTestClient(t, func(config *CmdConfig, tm *tcMocks) { + anthropic_key_id := "00000000-0000-4000-8000-000000000000" + config.Args = append(config.Args, anthropic_key_id) + tm.gradientAI.EXPECT().ListAgentsByAnthropicAPIKey("00000000-0000-4000-8000-000000000000").Return(do.Agents{}, nil) + err := RunAnthropicKeyGetAgents(config) + assert.NoError(t, err) + }) +} diff --git a/do/gradientai.go b/do/gradientai.go index 97a009d0e..8df2fc932 100644 --- a/do/gradientai.go +++ b/do/gradientai.go @@ -56,6 +56,13 @@ type OpenAiApiKey struct { // OpenAiApiKeys is a slice of OpenAiApiKey. type OpenAiApiKeys []OpenAiApiKey +type AnthropicApiKey struct { + *godo.AnthropicApiKeyInfo +} + +// AnthropicApiKeys is a slice of AnthropicApiKey. +type AnthropicApiKeys []AnthropicApiKey + // ApiKeys is a slice of ApiKey. type ApiKeys []ApiKeyInfo @@ -138,6 +145,12 @@ type GradientAIService interface { UpdateOpenAIAPIKey(openaiApiKeyId string, openaiAPIKeyUpdate *godo.OpenAIAPIKeyUpdateRequest) (*OpenAiApiKey, error) DeleteOpenAIAPIKey(openaiApiKeyId string) (*OpenAiApiKey, error) ListAgentsByOpenAIAPIKey(openaiApiKeyId string) (Agents, error) + ListAnthropicAPIKeys() (AnthropicApiKeys, error) + CreateAnthropicAPIKey(anthropicAPIKeyCreate *godo.AnthropicAPIKeyCreateRequest) (*AnthropicApiKey, error) + GetAnthropicAPIKey(anthropicApiKeyId string) (*AnthropicApiKey, error) + UpdateAnthropicAPIKey(anthropicApiKeyId string, anthropicAPIKeyUpdate *godo.AnthropicAPIKeyUpdateRequest) (*AnthropicApiKey, error) + DeleteAnthropicAPIKey(anthropicApiKeyId string) (*AnthropicApiKey, error) + ListAgentsByAnthropicAPIKey(anthropicApiKeyId string) (Agents, error) ListDatacenterRegions(servesInference, servesBatch *bool) (DatacenterRegions, error) ListAvailableModels() (Models, error) ListIndexingJobs() (IndexingJobs, error) @@ -594,6 +607,102 @@ func (a *gradientAIService) ListAgentsByOpenAIAPIKey(openaiApiKeyId string) (Age return list, nil } +func (a *gradientAIService) ListAnthropicAPIKeys() (AnthropicApiKeys, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + list, resp, err := a.client.GradientAI.ListAnthropicAPIKeys(context.TODO(), opt) + if err != nil { + return nil, nil, err + } + si := make([]any, len(list)) + for i := range list { + si[i] = list[i] + } + return si, resp, err + } + + si, err := PaginateResp(f) + if err != nil { + return nil, err + } + + list := make([]AnthropicApiKey, len(si)) + for i := range si { + ak := si[i].(*godo.AnthropicApiKeyInfo) + list[i] = AnthropicApiKey{AnthropicApiKeyInfo: ak} + } + + return list, nil +} + +func (a *gradientAIService) CreateAnthropicAPIKey(anthropicAPIKeyCreate *godo.AnthropicAPIKeyCreateRequest) (*AnthropicApiKey, error) { + anthropicApiKey, _, err := a.client.GradientAI.CreateAnthropicAPIKey(context.TODO(), anthropicAPIKeyCreate) + if err != nil { + return nil, err + } + return &AnthropicApiKey{AnthropicApiKeyInfo: anthropicApiKey}, nil +} + +func (a *gradientAIService) GetAnthropicAPIKey(anthropicApiKeyId string) (*AnthropicApiKey, error) { + anthropicApiKey, _, err := a.client.GradientAI.GetAnthropicAPIKey(context.TODO(), anthropicApiKeyId) + if err != nil { + return nil, err + } + return &AnthropicApiKey{AnthropicApiKeyInfo: anthropicApiKey}, nil +} + +func (a *gradientAIService) UpdateAnthropicAPIKey(anthropicApiKeyId string, anthropicAPIKeyUpdate *godo.AnthropicAPIKeyUpdateRequest) (*AnthropicApiKey, error) { + anthropicApiKey, _, err := a.client.GradientAI.UpdateAnthropicAPIKey(context.TODO(), anthropicApiKeyId, anthropicAPIKeyUpdate) + if err != nil { + return nil, err + } + return &AnthropicApiKey{AnthropicApiKeyInfo: anthropicApiKey}, nil +} + +func (a *gradientAIService) DeleteAnthropicAPIKey(anthropicApiKeyId string) (*AnthropicApiKey, error) { + anthropicApiKey, _, err := a.client.GradientAI.DeleteAnthropicAPIKey(context.TODO(), anthropicApiKeyId) + if err != nil { + return nil, err + } + return &AnthropicApiKey{AnthropicApiKeyInfo: anthropicApiKey}, nil +} + +func (a *gradientAIService) ListAgentsByAnthropicAPIKey(anthropicApiKeyId string) (Agents, error) { + f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { + agents, resp, err := a.client.GradientAI.ListAgentsByAnthropicAPIKey(context.TODO(), anthropicApiKeyId, opt) + if err != nil { + return nil, nil, err + } + list := make([]any, len(agents)) + for i := range agents { + list[i] = agents[i] + } + return list, resp, nil + } + + // Checking if there are no API keys we don't need to paginate + opt := &godo.ListOptions{Page: 1, PerPage: perPage} + res, _, err := f(opt) + if err != nil { + return nil, err + } + if len(res) == 0 { + return Agents{}, nil + } + + si, err := PaginateResp(f) + if err != nil { + return nil, err + } + + list := make([]Agent, len(si)) + for i := range si { + a := si[i].(*godo.Agent) + list[i] = Agent{Agent: a} + } + + return list, nil +} + func (a *gradientAIService) ListDatacenterRegions(servesInference, servesBatch *bool) (DatacenterRegions, error) { f := func(opt *godo.ListOptions) ([]any, *godo.Response, error) { list, resp, err := a.client.GradientAI.ListDatacenterRegions(context.TODO(), servesInference, servesBatch) diff --git a/do/mocks/GradientAIService.go b/do/mocks/GradientAIService.go index 03b6610ef..66c540918 100644 --- a/do/mocks/GradientAIService.go +++ b/do/mocks/GradientAIService.go @@ -131,6 +131,21 @@ func (mr *MockGradientAIServiceMockRecorder) CreateAgentAPIKey(agentID, req any) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAgentAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).CreateAgentAPIKey), agentID, req) } +// CreateAnthropicAPIKey mocks base method. +func (m *MockGradientAIService) CreateAnthropicAPIKey(anthropicAPIKeyCreate *godo.AnthropicAPIKeyCreateRequest) (*do.AnthropicApiKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CreateAnthropicAPIKey", anthropicAPIKeyCreate) + ret0, _ := ret[0].(*do.AnthropicApiKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CreateAnthropicAPIKey indicates an expected call of CreateAnthropicAPIKey. +func (mr *MockGradientAIServiceMockRecorder) CreateAnthropicAPIKey(anthropicAPIKeyCreate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateAnthropicAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).CreateAnthropicAPIKey), anthropicAPIKeyCreate) +} + // CreateFunctionRoute mocks base method. func (m *MockGradientAIService) CreateFunctionRoute(id string, req *godo.FunctionRouteCreateRequest) (*do.Agent, error) { m.ctrl.T.Helper() @@ -218,6 +233,21 @@ func (mr *MockGradientAIServiceMockRecorder) DeleteAgentRoute(parentAgentID, chi return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAgentRoute", reflect.TypeOf((*MockGradientAIService)(nil).DeleteAgentRoute), parentAgentID, childAgentID) } +// DeleteAnthropicAPIKey mocks base method. +func (m *MockGradientAIService) DeleteAnthropicAPIKey(anthropicApiKeyId string) (*do.AnthropicApiKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteAnthropicAPIKey", anthropicApiKeyId) + ret0, _ := ret[0].(*do.AnthropicApiKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteAnthropicAPIKey indicates an expected call of DeleteAnthropicAPIKey. +func (mr *MockGradientAIServiceMockRecorder) DeleteAnthropicAPIKey(anthropicApiKeyId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteAnthropicAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).DeleteAnthropicAPIKey), anthropicApiKeyId) +} + // DeleteFunctionRoute mocks base method. func (m *MockGradientAIService) DeleteFunctionRoute(agent_id, function_id string) (*do.Agent, error) { m.ctrl.T.Helper() @@ -306,6 +336,21 @@ func (mr *MockGradientAIServiceMockRecorder) GetAgent(agentID any) *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAgent", reflect.TypeOf((*MockGradientAIService)(nil).GetAgent), agentID) } +// GetAnthropicAPIKey mocks base method. +func (m *MockGradientAIService) GetAnthropicAPIKey(anthropicApiKeyId string) (*do.AnthropicApiKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetAnthropicAPIKey", anthropicApiKeyId) + ret0, _ := ret[0].(*do.AnthropicApiKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetAnthropicAPIKey indicates an expected call of GetAnthropicAPIKey. +func (mr *MockGradientAIServiceMockRecorder) GetAnthropicAPIKey(anthropicApiKeyId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAnthropicAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).GetAnthropicAPIKey), anthropicApiKeyId) +} + // GetIndexingJob mocks base method. func (m *MockGradientAIService) GetIndexingJob(indexingJobID string) (*do.IndexingJob, error) { m.ctrl.T.Helper() @@ -396,6 +441,21 @@ func (mr *MockGradientAIServiceMockRecorder) ListAgents() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAgents", reflect.TypeOf((*MockGradientAIService)(nil).ListAgents)) } +// ListAgentsByAnthropicAPIKey mocks base method. +func (m *MockGradientAIService) ListAgentsByAnthropicAPIKey(anthropicApiKeyId string) (do.Agents, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAgentsByAnthropicAPIKey", anthropicApiKeyId) + ret0, _ := ret[0].(do.Agents) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAgentsByAnthropicAPIKey indicates an expected call of ListAgentsByAnthropicAPIKey. +func (mr *MockGradientAIServiceMockRecorder) ListAgentsByAnthropicAPIKey(anthropicApiKeyId any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAgentsByAnthropicAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).ListAgentsByAnthropicAPIKey), anthropicApiKeyId) +} + // ListAgentsByOpenAIAPIKey mocks base method. func (m *MockGradientAIService) ListAgentsByOpenAIAPIKey(openaiApiKeyId string) (do.Agents, error) { m.ctrl.T.Helper() @@ -411,6 +471,21 @@ func (mr *MockGradientAIServiceMockRecorder) ListAgentsByOpenAIAPIKey(openaiApiK return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAgentsByOpenAIAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).ListAgentsByOpenAIAPIKey), openaiApiKeyId) } +// ListAnthropicAPIKeys mocks base method. +func (m *MockGradientAIService) ListAnthropicAPIKeys() (do.AnthropicApiKeys, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ListAnthropicAPIKeys") + ret0, _ := ret[0].(do.AnthropicApiKeys) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ListAnthropicAPIKeys indicates an expected call of ListAnthropicAPIKeys. +func (mr *MockGradientAIServiceMockRecorder) ListAnthropicAPIKeys() *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListAnthropicAPIKeys", reflect.TypeOf((*MockGradientAIService)(nil).ListAnthropicAPIKeys)) +} + // ListAvailableModels mocks base method. func (m *MockGradientAIService) ListAvailableModels() (do.Models, error) { m.ctrl.T.Helper() @@ -591,6 +666,21 @@ func (mr *MockGradientAIServiceMockRecorder) UpdateAgentVisibility(agentID, req return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAgentVisibility", reflect.TypeOf((*MockGradientAIService)(nil).UpdateAgentVisibility), agentID, req) } +// UpdateAnthropicAPIKey mocks base method. +func (m *MockGradientAIService) UpdateAnthropicAPIKey(anthropicApiKeyId string, anthropicAPIKeyUpdate *godo.AnthropicAPIKeyUpdateRequest) (*do.AnthropicApiKey, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "UpdateAnthropicAPIKey", anthropicApiKeyId, anthropicAPIKeyUpdate) + ret0, _ := ret[0].(*do.AnthropicApiKey) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// UpdateAnthropicAPIKey indicates an expected call of UpdateAnthropicAPIKey. +func (mr *MockGradientAIServiceMockRecorder) UpdateAnthropicAPIKey(anthropicApiKeyId, anthropicAPIKeyUpdate any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateAnthropicAPIKey", reflect.TypeOf((*MockGradientAIService)(nil).UpdateAnthropicAPIKey), anthropicApiKeyId, anthropicAPIKeyUpdate) +} + // UpdateFunctionRoute mocks base method. func (m *MockGradientAIService) UpdateFunctionRoute(agent_id, function_id string, req *godo.FunctionRouteUpdateRequest) (*do.Agent, error) { m.ctrl.T.Helper() diff --git a/integration/gradientai_anthropic_key_create_test.go b/integration/gradientai_anthropic_key_create_test.go new file mode 100644 index 000000000..1ac0fb7d3 --- /dev/null +++ b/integration/gradientai_anthropic_key_create_test.go @@ -0,0 +1,102 @@ +package integration + +import ( + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("gradient/anthropic-key/create", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/gen-ai/anthropic/keys": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(anthropicKeyResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("valid anthropic api key and name is passed", func() { + it("creates the anthropic key", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "create", + "--name", "api-key", + "--api-key", "sk-ant-prodajkbcsdovub", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + expect.Contains(strings.TrimSpace(string(output)), strings.TrimSpace(string(anthropicKeyOutput))) + }) + }) + + when("valid anthropic api key not passed", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "create", + "--api-key", "sk-ant-prodajkbcsdovub", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(strings.TrimSpace(string(output)), "missing required arguments") + }) + }) + + when("valid name not passed", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "create", + "--name", "api-key", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(strings.TrimSpace(string(output)), "missing required arguments") + }) + }) +}) diff --git a/integration/gradientai_anthropic_key_delete_test.go b/integration/gradientai_anthropic_key_delete_test.go new file mode 100644 index 000000000..b8c4cebd4 --- /dev/null +++ b/integration/gradientai_anthropic_key_delete_test.go @@ -0,0 +1,103 @@ +package integration + +import ( + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("gradient/anthropic-key/delete", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/gen-ai/anthropic/keys/00000000-0000-4000-8000-000000000000": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(anthropicKeyResponse)) + case "/v2/gen-ai/anthropic/keys/99999999-9999-4999-8999-999999999999": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodDelete { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{ + "id": "not_found", + "message": "failed to get anthropic api key" + }`)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("valid anthropic key id is passed", func() { + it("deletes the anthropic key", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "delete", + "00000000-0000-4000-8000-000000000000", + "-f", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + expect.Contains(strings.TrimSpace(string(output)), "Anthropic API Key deleted successfully") + }) + }) + + when("invalid anthropic key is passed", func() { + it("return not found error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "delete", + "99999999-9999-4999-8999-999999999999", + "-f", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(strings.TrimSpace(string(output)), "failed to get anthropic api key") + }) + }) +}) diff --git a/integration/gradientai_anthropic_key_get_test.go b/integration/gradientai_anthropic_key_get_test.go new file mode 100644 index 000000000..bf74f1158 --- /dev/null +++ b/integration/gradientai_anthropic_key_get_test.go @@ -0,0 +1,233 @@ +package integration + +import ( + "fmt" + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("gradient/anthropic-key/get", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/gen-ai/anthropic/keys/00000000-0000-4000-8000-000000000000": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(anthropicKeyResponse)) + case "/v2/gen-ai/anthropic/keys/99999999-9999-4999-8999-999999999999": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"id":"not_found","message":"The resource you requested could not be found."}`)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("anthropic key id is not passed", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "get", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "command is missing required arguments") + }) + }) + + when("anthropic key does not exist", func() { + it("returns a not found error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "get", + "99999999-9999-4999-8999-999999999999", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "The resource you requested could not be found.") + }) + }) + + when("valid anthropic key id is passed", func() { + it("gets the anthropic key", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "get", + "00000000-0000-4000-8000-000000000000", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + expect.Contains(strings.TrimSpace(string(anthropicKeyOutput)), strings.TrimSpace(string(output))) + }) + }) +}) + +var _ = suite("gradient/anthropic-key/get-agents", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/gen-ai/anthropic/keys/00000000-0000-4000-8000-000000000000/agents": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(agentListResponse)) + case "/v2/gen-ai/anthropic/keys/99999999-9999-4999-8999-999999999999/agents": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{"id":"not_found","message":"The resource you requested could not be found."}`)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("anthropic key id is not passed", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "get-agents", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "command is missing required arguments") + }) + }) + + when("anthropic key does not exist", func() { + it("returns a not found error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "get-agents", + "99999999-9999-4999-8999-999999999999", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(string(output), "The resource you requested could not be found.") + }) + }) + + when("valid anthropic key id is passed", func() { + it("gets the agents using the anthropic key", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "get-agents", + "00000000-0000-4000-8000-000000000000", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err, fmt.Sprintf("received error output: %s", output)) + expect.Contains(strings.TrimSpace(string(agentListOutput)), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + anthropicKeyResponse = ` + { + "api_key_info": { + "name": "api-key", + "uuid": "11f066ea-0000-0000-0000-4e013e2ddde4", + "created_by": "18919793", + "created_at": "2025-07-22T10:57:42Z", + "updated_at": "2025-07-22T10:57:42Z" + } + }` + + anthropicKeyOutput = ` +Name UUID Created At Created By Updated At Deleted At +api-key 11f066ea-0000-0000-0000-4e013e2ddde4 2025-07-22 10:57:42 +0000 UTC 18919793 2025-07-22 10:57:42 +0000 UTC ` +) diff --git a/integration/gradientai_anthropic_key_list_test.go b/integration/gradientai_anthropic_key_list_test.go new file mode 100644 index 000000000..bbe93590b --- /dev/null +++ b/integration/gradientai_anthropic_key_list_test.go @@ -0,0 +1,85 @@ +package integration + +import ( + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("gradient/anthropic-key/list", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/gen-ai/anthropic/keys": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodGet { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(anthropicKeyListResponse)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("valid list anthropic keys", func() { + it("gets the list of anthropic key", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "list", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + expect.Contains(strings.TrimSpace(string(anthropicKeyListOutput)), strings.TrimSpace(string(output))) + }) + }) +}) + +const ( + anthropicKeyListResponse = ` + { + "api_key_infos": [ + { + "name": "new key", + "uuid": "11f037b5-0000-0000-0000-4e013e2ddde4", + "created_by": "18919793", + "created_at": "2025-05-23T09:09:25Z", + "updated_at": "2025-07-08T05:33:17Z" + } + ] +}` + + anthropicKeyListOutput = ` +Name UUID Created At Created By Updated At Deleted At +new key 11f037b5-0000-0000-0000-4e013e2ddde4 2025-05-23 09:09:25 +0000 UTC 18919793 2025-07-08 05:33:17 +0000 UTC ` +) diff --git a/integration/gradientai_anthropic_key_update_test.go b/integration/gradientai_anthropic_key_update_test.go new file mode 100644 index 000000000..983d41fee --- /dev/null +++ b/integration/gradientai_anthropic_key_update_test.go @@ -0,0 +1,103 @@ +package integration + +import ( + "net/http" + "net/http/httptest" + "net/http/httputil" + "os/exec" + "strings" + "testing" + + "github.com/sclevine/spec" + "github.com/stretchr/testify/require" +) + +var _ = suite("gradient/anthropic-key/update", func(t *testing.T, when spec.G, it spec.S) { + var ( + expect *require.Assertions + cmd *exec.Cmd + server *httptest.Server + ) + + it.Before(func() { + expect = require.New(t) + + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + switch req.URL.Path { + case "/v2/gen-ai/anthropic/keys/00000000-0000-4000-8000-000000000000": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPut { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.Write([]byte(anthropicKeyResponse)) + case "/v2/gen-ai/anthropic/keys/99999999-9999-4999-8999-999999999999": + auth := req.Header.Get("Authorization") + if auth != "Bearer some-magic-token" { + w.WriteHeader(http.StatusUnauthorized) + return + } + + if req.Method != http.MethodPut { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + w.WriteHeader(http.StatusNotFound) + w.Write([]byte(`{ + "id": "not_found", + "message": "failed to get anthropic api key" + }`)) + default: + dump, err := httputil.DumpRequest(req, true) + if err != nil { + t.Fatal("failed to dump request") + } + + t.Fatalf("received unknown request: %s", dump) + } + })) + }) + + when("valid anthropic key id is passed", func() { + it("updates the anthropic key", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "update", + "00000000-0000-4000-8000-000000000000", + "--name", "api-key", + ) + + output, err := cmd.CombinedOutput() + expect.NoError(err) + expect.Contains(strings.TrimSpace(string(output)), strings.TrimSpace(string(anthropicKeyOutput))) + }) + }) + + when("invalid anthropic key id is passed", func() { + it("returns an error", func() { + cmd = exec.Command(builtBinaryPath, + "-t", "some-magic-token", + "-u", server.URL, + "gradient", + "anthropic-key", + "update", + "99999999-9999-4999-8999-999999999999", + "--name", "api-key", + ) + + output, err := cmd.CombinedOutput() + expect.Error(err) + expect.Contains(strings.TrimSpace(string(output)), "failed to get anthropic api key") + }) + }) +})