From a26ba2eefe1c3f9937cd0499ab468561aef69ee5 Mon Sep 17 00:00:00 2001 From: Wallace Breza Date: Wed, 21 Jan 2026 18:37:49 -0800 Subject: [PATCH] Expose more configuration for PromptResourceGroup --- cli/azd/.vscode/cspell.yaml | 1 + .../microsoft.azd.demo/internal/cmd/prompt.go | 5 + cli/azd/grpc/proto/prompt.proto | 7 + cli/azd/internal/grpcserver/prompt_service.go | 27 +- .../grpcserver/prompt_service_test.go | 542 ++++++++++++++++++ cli/azd/pkg/azdext/prompt.pb.go | 190 ++++-- .../mocks/mockprompt/mock_prompt_service.go | 76 +++ 7 files changed, 791 insertions(+), 57 deletions(-) create mode 100644 cli/azd/internal/grpcserver/prompt_service_test.go create mode 100644 cli/azd/test/mocks/mockprompt/mock_prompt_service.go diff --git a/cli/azd/.vscode/cspell.yaml b/cli/azd/.vscode/cspell.yaml index 193ead0274e..ee7c11f59d6 100644 --- a/cli/azd/.vscode/cspell.yaml +++ b/cli/azd/.vscode/cspell.yaml @@ -8,6 +8,7 @@ words: - Canonicalize - Chans - chinacloudapi + - cmds - Codespace - Codespaces - cooldown diff --git a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/prompt.go b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/prompt.go index e6db7f2d06c..8d994c9f83b 100644 --- a/cli/azd/extensions/microsoft.azd.demo/internal/cmd/prompt.go +++ b/cli/azd/extensions/microsoft.azd.demo/internal/cmd/prompt.go @@ -204,6 +204,11 @@ func newPromptCommand() *cobra.Command { Prompt(). PromptResourceGroup(ctx, &azdext.PromptResourceGroupRequest{ AzureContext: &azureContext, + Options: &azdext.PromptResourceGroupOptions{ + SelectOptions: &azdext.PromptResourceSelectOptions{ + AllowNewResource: to.Ptr(false), + }, + }, }) if err != nil { return err diff --git a/cli/azd/grpc/proto/prompt.proto b/cli/azd/grpc/proto/prompt.proto index f8d67fe999a..d226a240167 100644 --- a/cli/azd/grpc/proto/prompt.proto +++ b/cli/azd/grpc/proto/prompt.proto @@ -56,6 +56,7 @@ message PromptLocationResponse { message PromptResourceGroupRequest { AzureContext azure_context = 1; + PromptResourceGroupOptions options = 2; } message PromptResourceGroupResponse { @@ -182,4 +183,10 @@ message PromptResourceSelectOptions { string loading_message = 7; optional bool display_numbers = 8; int32 display_count = 9; + string hint = 10; + optional bool enable_filtering = 11; +} + +message PromptResourceGroupOptions { + PromptResourceSelectOptions select_options = 1; } diff --git a/cli/azd/internal/grpcserver/prompt_service.go b/cli/azd/internal/grpcserver/prompt_service.go index de368741567..2c89de7ee0a 100644 --- a/cli/azd/internal/grpcserver/prompt_service.go +++ b/cli/azd/internal/grpcserver/prompt_service.go @@ -289,7 +289,9 @@ func (s *promptService) PromptResourceGroup( return nil, err } - selectedResourceGroup, err := s.prompter.PromptResourceGroup(ctx, azureContext, nil) + options := createResourceGroupOptions(req.Options) + + selectedResourceGroup, err := s.prompter.PromptResourceGroup(ctx, azureContext, options) if err != nil { return nil, err } @@ -418,6 +420,8 @@ func createResourceOptions(options *azdext.PromptResourceOptions) prompt.Resourc DisplayCount: int(options.SelectOptions.DisplayCount), DisplayNumbers: options.SelectOptions.DisplayNumbers, AllowNewResource: options.SelectOptions.AllowNewResource, + Hint: options.SelectOptions.Hint, + EnableFiltering: options.SelectOptions.EnableFiltering, } } @@ -431,6 +435,27 @@ func createResourceOptions(options *azdext.PromptResourceOptions) prompt.Resourc return resourceOptions } +func createResourceGroupOptions(options *azdext.PromptResourceGroupOptions) *prompt.ResourceGroupOptions { + if options == nil || options.SelectOptions == nil { + return nil + } + + return &prompt.ResourceGroupOptions{ + SelectorOptions: &prompt.SelectOptions{ + ForceNewResource: options.SelectOptions.ForceNewResource, + AllowNewResource: options.SelectOptions.AllowNewResource, + NewResourceMessage: options.SelectOptions.NewResourceMessage, + Message: options.SelectOptions.Message, + HelpMessage: options.SelectOptions.HelpMessage, + LoadingMessage: options.SelectOptions.LoadingMessage, + DisplayCount: int(options.SelectOptions.DisplayCount), + DisplayNumbers: options.SelectOptions.DisplayNumbers, + Hint: options.SelectOptions.Hint, + EnableFiltering: options.SelectOptions.EnableFiltering, + }, + } +} + func convertToInt32(input *int) *int32 { if input == nil { return nil // Handle the nil case diff --git a/cli/azd/internal/grpcserver/prompt_service_test.go b/cli/azd/internal/grpcserver/prompt_service_test.go new file mode 100644 index 00000000000..52d7f481f09 --- /dev/null +++ b/cli/azd/internal/grpcserver/prompt_service_test.go @@ -0,0 +1,542 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package grpcserver + +import ( + "context" + "testing" + + "github.com/Azure/azure-sdk-for-go/sdk/azcore/to" + "github.com/azure/azure-dev/cli/azd/internal" + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/azdext" + "github.com/azure/azure-dev/cli/azd/pkg/prompt" + "github.com/azure/azure-dev/cli/azd/test/mocks/mockprompt" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func Test_PromptService_Confirm_NoPromptWithDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + resp, err := service.Confirm(context.Background(), &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + Message: "Continue?", + DefaultValue: to.Ptr(true), + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.Value) + require.True(t, *resp.Value) +} + +func Test_PromptService_Confirm_NoPromptWithoutDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + _, err := service.Confirm(context.Background(), &azdext.ConfirmRequest{ + Options: &azdext.ConfirmOptions{ + Message: "Continue?", + }, + }) + + require.Error(t, err) + require.Contains(t, err.Error(), "no default response") +} + +func Test_PromptService_Select_NoPromptWithDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + resp, err := service.Select(context.Background(), &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Choose option:", + SelectedIndex: to.Ptr(int32(1)), + Choices: []*azdext.SelectChoice{ + {Value: "a", Label: "Option A"}, + {Value: "b", Label: "Option B"}, + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.Value) + require.Equal(t, int32(1), *resp.Value) +} + +func Test_PromptService_Select_NoPromptWithoutDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + _, err := service.Select(context.Background(), &azdext.SelectRequest{ + Options: &azdext.SelectOptions{ + Message: "Choose option:", + Choices: []*azdext.SelectChoice{ + {Value: "a", Label: "Option A"}, + }, + }, + }) + + require.Error(t, err) + require.Contains(t, err.Error(), "no default selection") +} + +func Test_PromptService_MultiSelect_NoPrompt(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + resp, err := service.MultiSelect(context.Background(), &azdext.MultiSelectRequest{ + Options: &azdext.MultiSelectOptions{ + Message: "Select items:", + Choices: []*azdext.MultiSelectChoice{ + {Value: "a", Label: "Option A", Selected: true}, + {Value: "b", Label: "Option B", Selected: false}, + {Value: "c", Label: "Option C", Selected: true}, + }, + }, + }) + + require.NoError(t, err) + require.Len(t, resp.Values, 2) + require.Equal(t, "a", resp.Values[0].Value) + require.Equal(t, "c", resp.Values[1].Value) +} + +func Test_PromptService_Prompt_NoPromptWithDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + resp, err := service.Prompt(context.Background(), &azdext.PromptRequest{ + Options: &azdext.PromptOptions{ + Message: "Enter name:", + DefaultValue: "default-name", + Required: true, + }, + }) + + require.NoError(t, err) + require.Equal(t, "default-name", resp.Value) +} + +func Test_PromptService_Prompt_NoPromptRequiredWithoutDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + _, err := service.Prompt(context.Background(), &azdext.PromptRequest{ + Options: &azdext.PromptOptions{ + Message: "Enter name:", + Required: true, + }, + }) + + require.Error(t, err) + require.Contains(t, err.Error(), "no default response") +} + +func Test_PromptService_Prompt_NoPromptNotRequiredWithoutDefault(t *testing.T) { + globalOptions := &internal.GlobalCommandOptions{NoPrompt: true} + service := NewPromptService(nil, nil, globalOptions) + + resp, err := service.Prompt(context.Background(), &azdext.PromptRequest{ + Options: &azdext.PromptOptions{ + Message: "Enter name:", + Required: false, + }, + }) + + require.NoError(t, err) + require.Equal(t, "", resp.Value) +} + +func Test_PromptService_PromptSubscription(t *testing.T) { + mockPrompter := &mockprompt.MockPromptService{} + globalOptions := &internal.GlobalCommandOptions{NoPrompt: false} + + expectedSub := &account.Subscription{ + Id: "/subscriptions/sub-123", + Name: "Test Subscription", + TenantId: "tenant-123", + } + + mockPrompter. + On("PromptSubscription", mock.Anything, mock.Anything). + Return(expectedSub, nil) + + service := NewPromptService(mockPrompter, nil, globalOptions) + + resp, err := service.PromptSubscription(context.Background(), &azdext.PromptSubscriptionRequest{ + Message: "Select subscription:", + HelpMessage: "Choose your subscription", + }) + + require.NoError(t, err) + require.NotNil(t, resp.Subscription) + require.Equal(t, expectedSub.Id, resp.Subscription.Id) + require.Equal(t, expectedSub.Name, resp.Subscription.Name) + require.Equal(t, expectedSub.TenantId, resp.Subscription.TenantId) + mockPrompter.AssertExpectations(t) +} + +func Test_PromptService_PromptLocation(t *testing.T) { + mockPrompter := &mockprompt.MockPromptService{} + globalOptions := &internal.GlobalCommandOptions{NoPrompt: false} + + expectedLocation := &account.Location{ + Name: "eastus", + DisplayName: "East US", + RegionalDisplayName: "(US) East US", + } + + mockPrompter. + On("PromptLocation", mock.Anything, mock.Anything, mock.Anything). + Return(expectedLocation, nil) + + service := NewPromptService(mockPrompter, nil, globalOptions) + + resp, err := service.PromptLocation(context.Background(), &azdext.PromptLocationRequest{ + AzureContext: &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: "sub-123", + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.Location) + require.Equal(t, expectedLocation.Name, resp.Location.Name) + require.Equal(t, expectedLocation.DisplayName, resp.Location.DisplayName) + require.Equal(t, expectedLocation.RegionalDisplayName, resp.Location.RegionalDisplayName) + mockPrompter.AssertExpectations(t) +} + +func Test_PromptService_PromptResourceGroup(t *testing.T) { + mockPrompter := &mockprompt.MockPromptService{} + globalOptions := &internal.GlobalCommandOptions{NoPrompt: false} + + expectedRg := &azapi.ResourceGroup{ + Id: "/subscriptions/sub-123/resourceGroups/rg-test", + Name: "rg-test", + Location: "eastus", + } + + mockPrompter. + On("PromptResourceGroup", mock.Anything, mock.Anything, mock.MatchedBy(func(opts *prompt.ResourceGroupOptions) bool { + return opts != nil && + opts.SelectorOptions != nil && + opts.SelectorOptions.AllowNewResource != nil && + *opts.SelectorOptions.AllowNewResource == false && + opts.SelectorOptions.Message == "Select resource group" && + opts.SelectorOptions.EnableFiltering != nil && + *opts.SelectorOptions.EnableFiltering == true + })). + Return(expectedRg, nil) + + service := NewPromptService(mockPrompter, nil, globalOptions) + + resp, err := service.PromptResourceGroup(context.Background(), &azdext.PromptResourceGroupRequest{ + AzureContext: &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: "sub-123", + }, + }, + Options: &azdext.PromptResourceGroupOptions{ + SelectOptions: &azdext.PromptResourceSelectOptions{ + AllowNewResource: to.Ptr(false), + Message: "Select resource group", + EnableFiltering: to.Ptr(true), + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.ResourceGroup) + require.Equal(t, expectedRg.Id, resp.ResourceGroup.Id) + require.Equal(t, expectedRg.Name, resp.ResourceGroup.Name) + require.Equal(t, expectedRg.Location, resp.ResourceGroup.Location) + mockPrompter.AssertExpectations(t) +} + +func Test_PromptService_PromptResourceGroup_NilOptions(t *testing.T) { + mockPrompter := &mockprompt.MockPromptService{} + globalOptions := &internal.GlobalCommandOptions{NoPrompt: false} + + expectedRg := &azapi.ResourceGroup{ + Id: "/subscriptions/sub-123/resourceGroups/rg-test", + Name: "rg-test", + Location: "eastus", + } + + mockPrompter. + On("PromptResourceGroup", mock.Anything, mock.Anything, (*prompt.ResourceGroupOptions)(nil)). + Return(expectedRg, nil) + + service := NewPromptService(mockPrompter, nil, globalOptions) + + resp, err := service.PromptResourceGroup(context.Background(), &azdext.PromptResourceGroupRequest{ + AzureContext: &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: "sub-123", + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.ResourceGroup) + mockPrompter.AssertExpectations(t) +} + +func Test_PromptService_PromptSubscriptionResource(t *testing.T) { + mockPrompter := &mockprompt.MockPromptService{} + globalOptions := &internal.GlobalCommandOptions{NoPrompt: false} + + expectedResource := &azapi.ResourceExtended{ + Resource: azapi.Resource{ + Id: "/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.Storage/storageAccounts/storage1", + Name: "storage1", + Type: "Microsoft.Storage/storageAccounts", + Location: "eastus", + }, + Kind: "StorageV2", + } + + mockPrompter. + On( + "PromptSubscriptionResource", + mock.Anything, + mock.Anything, + mock.MatchedBy(func(opts prompt.ResourceOptions) bool { + return opts.ResourceType != nil && + *opts.ResourceType == azapi.AzureResourceType("Microsoft.Storage/storageAccounts") && + len(opts.Kinds) == 2 && + opts.Kinds[0] == "StorageV2" && + opts.SelectorOptions != nil && + opts.SelectorOptions.AllowNewResource != nil && + *opts.SelectorOptions.AllowNewResource == false && + opts.SelectorOptions.Hint == "Filter storage accounts" + }), + ). + Return(expectedResource, nil) + + service := NewPromptService(mockPrompter, nil, globalOptions) + + resp, err := service.PromptSubscriptionResource(context.Background(), &azdext.PromptSubscriptionResourceRequest{ + AzureContext: &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: "sub-123", + }, + }, + Options: &azdext.PromptResourceOptions{ + ResourceType: "Microsoft.Storage/storageAccounts", + Kinds: []string{"StorageV2", "BlobStorage"}, + SelectOptions: &azdext.PromptResourceSelectOptions{ + AllowNewResource: to.Ptr(false), + Hint: "Filter storage accounts", + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.Resource) + require.Equal(t, expectedResource.Id, resp.Resource.Id) + require.Equal(t, expectedResource.Name, resp.Resource.Name) + require.Equal(t, expectedResource.Type, resp.Resource.Type) + require.Equal(t, expectedResource.Location, resp.Resource.Location) + require.Equal(t, expectedResource.Kind, resp.Resource.Kind) + mockPrompter.AssertExpectations(t) +} + +func Test_PromptService_PromptResourceGroupResource(t *testing.T) { + mockPrompter := &mockprompt.MockPromptService{} + globalOptions := &internal.GlobalCommandOptions{NoPrompt: false} + + expectedResource := &azapi.ResourceExtended{ + Resource: azapi.Resource{ + Id: "/subscriptions/sub-123/resourceGroups/rg-test/providers/Microsoft.Web/sites/webapp1", + Name: "webapp1", + Type: "Microsoft.Web/sites", + Location: "eastus", + }, + } + + mockPrompter. + On( + "PromptResourceGroupResource", + mock.Anything, + mock.Anything, + mock.MatchedBy(func(opts prompt.ResourceOptions) bool { + return opts.ResourceType != nil && + *opts.ResourceType == azapi.AzureResourceType("Microsoft.Web/sites") && + opts.ResourceTypeDisplayName == "Web App" && + opts.SelectorOptions != nil && + opts.SelectorOptions.Message == "Select a web app" && + opts.SelectorOptions.EnableFiltering != nil && + *opts.SelectorOptions.EnableFiltering == true + }), + ). + Return(expectedResource, nil) + + service := NewPromptService(mockPrompter, nil, globalOptions) + + resp, err := service.PromptResourceGroupResource(context.Background(), &azdext.PromptResourceGroupResourceRequest{ + AzureContext: &azdext.AzureContext{ + Scope: &azdext.AzureScope{ + SubscriptionId: "sub-123", + ResourceGroup: "rg-test", + }, + }, + Options: &azdext.PromptResourceOptions{ + ResourceType: "Microsoft.Web/sites", + ResourceTypeDisplayName: "Web App", + SelectOptions: &azdext.PromptResourceSelectOptions{ + Message: "Select a web app", + EnableFiltering: to.Ptr(true), + }, + }, + }) + + require.NoError(t, err) + require.NotNil(t, resp.Resource) + require.Equal(t, expectedResource.Id, resp.Resource.Id) + require.Equal(t, expectedResource.Name, resp.Resource.Name) + require.Equal(t, expectedResource.Type, resp.Resource.Type) + mockPrompter.AssertExpectations(t) +} + +func Test_CreateResourceGroupOptions(t *testing.T) { + tests := []struct { + name string + input *azdext.PromptResourceGroupOptions + expected *prompt.ResourceGroupOptions + }{ + { + name: "nil options", + input: nil, + expected: nil, + }, + { + name: "nil select options", + input: &azdext.PromptResourceGroupOptions{ + SelectOptions: nil, + }, + expected: nil, + }, + { + name: "with all options", + input: &azdext.PromptResourceGroupOptions{ + SelectOptions: &azdext.PromptResourceSelectOptions{ + ForceNewResource: to.Ptr(true), + AllowNewResource: to.Ptr(false), + NewResourceMessage: "Create new RG", + Message: "Select RG", + HelpMessage: "Help text", + LoadingMessage: "Loading...", + DisplayNumbers: to.Ptr(true), + DisplayCount: 10, + Hint: "Hint text", + EnableFiltering: to.Ptr(true), + }, + }, + expected: &prompt.ResourceGroupOptions{ + SelectorOptions: &prompt.SelectOptions{ + ForceNewResource: to.Ptr(true), + AllowNewResource: to.Ptr(false), + NewResourceMessage: "Create new RG", + Message: "Select RG", + HelpMessage: "Help text", + LoadingMessage: "Loading...", + DisplayNumbers: to.Ptr(true), + DisplayCount: 10, + Hint: "Hint text", + EnableFiltering: to.Ptr(true), + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := createResourceGroupOptions(tt.input) + + if tt.expected == nil { + require.Nil(t, result) + return + } + + require.NotNil(t, result) + require.NotNil(t, result.SelectorOptions) + require.Equal(t, tt.expected.SelectorOptions.ForceNewResource, result.SelectorOptions.ForceNewResource) + require.Equal(t, tt.expected.SelectorOptions.AllowNewResource, result.SelectorOptions.AllowNewResource) + require.Equal(t, tt.expected.SelectorOptions.NewResourceMessage, result.SelectorOptions.NewResourceMessage) + require.Equal(t, tt.expected.SelectorOptions.Message, result.SelectorOptions.Message) + require.Equal(t, tt.expected.SelectorOptions.HelpMessage, result.SelectorOptions.HelpMessage) + require.Equal(t, tt.expected.SelectorOptions.LoadingMessage, result.SelectorOptions.LoadingMessage) + require.Equal(t, tt.expected.SelectorOptions.DisplayNumbers, result.SelectorOptions.DisplayNumbers) + require.Equal(t, tt.expected.SelectorOptions.DisplayCount, result.SelectorOptions.DisplayCount) + require.Equal(t, tt.expected.SelectorOptions.Hint, result.SelectorOptions.Hint) + require.Equal(t, tt.expected.SelectorOptions.EnableFiltering, result.SelectorOptions.EnableFiltering) + }) + } +} + +func Test_CreateResourceOptions(t *testing.T) { + tests := []struct { + name string + input *azdext.PromptResourceOptions + }{ + { + name: "nil options", + input: nil, + }, + { + name: "with resource type", + input: &azdext.PromptResourceOptions{ + ResourceType: "Microsoft.Storage/storageAccounts", + Kinds: []string{"StorageV2", "BlobStorage"}, + ResourceTypeDisplayName: "Storage Account", + }, + }, + { + name: "with select options", + input: &azdext.PromptResourceOptions{ + ResourceType: "Microsoft.Web/sites", + SelectOptions: &azdext.PromptResourceSelectOptions{ + AllowNewResource: to.Ptr(true), + Message: "Select web app", + EnableFiltering: to.Ptr(true), + Hint: "Filter by name", + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := createResourceOptions(tt.input) + + if tt.input == nil { + require.Equal(t, prompt.ResourceOptions{}, result) + return + } + + if tt.input.ResourceType != "" { + require.NotNil(t, result.ResourceType) + require.Equal(t, azapi.AzureResourceType(tt.input.ResourceType), *result.ResourceType) + } + + require.Equal(t, tt.input.Kinds, result.Kinds) + require.Equal(t, tt.input.ResourceTypeDisplayName, result.ResourceTypeDisplayName) + + if tt.input.SelectOptions != nil { + require.NotNil(t, result.SelectorOptions) + require.Equal(t, tt.input.SelectOptions.AllowNewResource, result.SelectorOptions.AllowNewResource) + require.Equal(t, tt.input.SelectOptions.Message, result.SelectorOptions.Message) + require.Equal(t, tt.input.SelectOptions.EnableFiltering, result.SelectorOptions.EnableFiltering) + require.Equal(t, tt.input.SelectOptions.Hint, result.SelectorOptions.Hint) + } + }) + } +} diff --git a/cli/azd/pkg/azdext/prompt.pb.go b/cli/azd/pkg/azdext/prompt.pb.go index dffe32d4ecb..027dff5bb83 100644 --- a/cli/azd/pkg/azdext/prompt.pb.go +++ b/cli/azd/pkg/azdext/prompt.pb.go @@ -209,8 +209,9 @@ func (x *PromptLocationResponse) GetLocation() *Location { } type PromptResourceGroupRequest struct { - state protoimpl.MessageState `protogen:"open.v1"` - AzureContext *AzureContext `protobuf:"bytes,1,opt,name=azure_context,json=azureContext,proto3" json:"azure_context,omitempty"` + state protoimpl.MessageState `protogen:"open.v1"` + AzureContext *AzureContext `protobuf:"bytes,1,opt,name=azure_context,json=azureContext,proto3" json:"azure_context,omitempty"` + Options *PromptResourceGroupOptions `protobuf:"bytes,2,opt,name=options,proto3" json:"options,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -252,6 +253,13 @@ func (x *PromptResourceGroupRequest) GetAzureContext() *AzureContext { return nil } +func (x *PromptResourceGroupRequest) GetOptions() *PromptResourceGroupOptions { + if x != nil { + return x.Options + } + return nil +} + type PromptResourceGroupResponse struct { state protoimpl.MessageState `protogen:"open.v1"` ResourceGroup *ResourceGroup `protobuf:"bytes,1,opt,name=resource_group,json=resourceGroup,proto3" json:"resource_group,omitempty"` @@ -1415,6 +1423,8 @@ type PromptResourceSelectOptions struct { LoadingMessage string `protobuf:"bytes,7,opt,name=loading_message,json=loadingMessage,proto3" json:"loading_message,omitempty"` DisplayNumbers *bool `protobuf:"varint,8,opt,name=display_numbers,json=displayNumbers,proto3,oneof" json:"display_numbers,omitempty"` DisplayCount int32 `protobuf:"varint,9,opt,name=display_count,json=displayCount,proto3" json:"display_count,omitempty"` + Hint string `protobuf:"bytes,10,opt,name=hint,proto3" json:"hint,omitempty"` + EnableFiltering *bool `protobuf:"varint,11,opt,name=enable_filtering,json=enableFiltering,proto3,oneof" json:"enable_filtering,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -1512,6 +1522,64 @@ func (x *PromptResourceSelectOptions) GetDisplayCount() int32 { return 0 } +func (x *PromptResourceSelectOptions) GetHint() string { + if x != nil { + return x.Hint + } + return "" +} + +func (x *PromptResourceSelectOptions) GetEnableFiltering() bool { + if x != nil && x.EnableFiltering != nil { + return *x.EnableFiltering + } + return false +} + +type PromptResourceGroupOptions struct { + state protoimpl.MessageState `protogen:"open.v1"` + SelectOptions *PromptResourceSelectOptions `protobuf:"bytes,1,opt,name=select_options,json=selectOptions,proto3" json:"select_options,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *PromptResourceGroupOptions) Reset() { + *x = PromptResourceGroupOptions{} + mi := &file_prompt_proto_msgTypes[26] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *PromptResourceGroupOptions) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*PromptResourceGroupOptions) ProtoMessage() {} + +func (x *PromptResourceGroupOptions) ProtoReflect() protoreflect.Message { + mi := &file_prompt_proto_msgTypes[26] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use PromptResourceGroupOptions.ProtoReflect.Descriptor instead. +func (*PromptResourceGroupOptions) Descriptor() ([]byte, []int) { + return file_prompt_proto_rawDescGZIP(), []int{26} +} + +func (x *PromptResourceGroupOptions) GetSelectOptions() *PromptResourceSelectOptions { + if x != nil { + return x.SelectOptions + } + return nil +} + var File_prompt_proto protoreflect.FileDescriptor const file_prompt_proto_rawDesc = "" + @@ -1525,9 +1593,10 @@ const file_prompt_proto_rawDesc = "" + "\x15PromptLocationRequest\x129\n" + "\razure_context\x18\x01 \x01(\v2\x14.azdext.AzureContextR\fazureContext\"F\n" + "\x16PromptLocationResponse\x12,\n" + - "\blocation\x18\x01 \x01(\v2\x10.azdext.LocationR\blocation\"W\n" + + "\blocation\x18\x01 \x01(\v2\x10.azdext.LocationR\blocation\"\x95\x01\n" + "\x1aPromptResourceGroupRequest\x129\n" + - "\razure_context\x18\x01 \x01(\v2\x14.azdext.AzureContextR\fazureContext\"[\n" + + "\razure_context\x18\x01 \x01(\v2\x14.azdext.AzureContextR\fazureContext\x12<\n" + + "\aoptions\x18\x02 \x01(\v2\".azdext.PromptResourceGroupOptionsR\aoptions\"[\n" + "\x1bPromptResourceGroupResponse\x12<\n" + "\x0eresource_group\x18\x01 \x01(\v2\x15.azdext.ResourceGroupR\rresourceGroup\"B\n" + "\x0eConfirmRequest\x120\n" + @@ -1610,7 +1679,7 @@ const file_prompt_proto_rawDesc = "" + "\rresource_type\x18\x01 \x01(\tR\fresourceType\x12\x14\n" + "\x05kinds\x18\x02 \x03(\tR\x05kinds\x12;\n" + "\x1aresource_type_display_name\x18\x03 \x01(\tR\x17resourceTypeDisplayName\x12J\n" + - "\x0eselect_options\x18\x04 \x01(\v2#.azdext.PromptResourceSelectOptionsR\rselectOptions\"\xdb\x03\n" + + "\x0eselect_options\x18\x04 \x01(\v2#.azdext.PromptResourceSelectOptionsR\rselectOptions\"\xb4\x04\n" + "\x1bPromptResourceSelectOptions\x121\n" + "\x12force_new_resource\x18\x01 \x01(\bH\x00R\x10forceNewResource\x88\x01\x01\x121\n" + "\x12allow_new_resource\x18\x02 \x01(\bH\x01R\x10allowNewResource\x88\x01\x01\x120\n" + @@ -1620,10 +1689,16 @@ const file_prompt_proto_rawDesc = "" + "\fhelp_message\x18\x06 \x01(\tR\vhelpMessage\x12'\n" + "\x0floading_message\x18\a \x01(\tR\x0eloadingMessage\x12,\n" + "\x0fdisplay_numbers\x18\b \x01(\bH\x02R\x0edisplayNumbers\x88\x01\x01\x12#\n" + - "\rdisplay_count\x18\t \x01(\x05R\fdisplayCountB\x15\n" + + "\rdisplay_count\x18\t \x01(\x05R\fdisplayCount\x12\x12\n" + + "\x04hint\x18\n" + + " \x01(\tR\x04hint\x12.\n" + + "\x10enable_filtering\x18\v \x01(\bH\x03R\x0fenableFiltering\x88\x01\x01B\x15\n" + "\x13_force_new_resourceB\x15\n" + "\x13_allow_new_resourceB\x12\n" + - "\x10_display_numbers2\x80\x06\n" + + "\x10_display_numbersB\x13\n" + + "\x11_enable_filtering\"h\n" + + "\x1aPromptResourceGroupOptions\x12J\n" + + "\x0eselect_options\x18\x01 \x01(\v2#.azdext.PromptResourceSelectOptionsR\rselectOptions2\x80\x06\n" + "\rPromptService\x12[\n" + "\x12PromptSubscription\x12!.azdext.PromptSubscriptionRequest\x1a\".azdext.PromptSubscriptionResponse\x12O\n" + "\x0ePromptLocation\x12\x1d.azdext.PromptLocationRequest\x1a\x1e.azdext.PromptLocationResponse\x12^\n" + @@ -1647,7 +1722,7 @@ func file_prompt_proto_rawDescGZIP() []byte { return file_prompt_proto_rawDescData } -var file_prompt_proto_msgTypes = make([]protoimpl.MessageInfo, 26) +var file_prompt_proto_msgTypes = make([]protoimpl.MessageInfo, 27) var file_prompt_proto_goTypes = []any{ (*PromptSubscriptionRequest)(nil), // 0: azdext.PromptSubscriptionRequest (*PromptSubscriptionResponse)(nil), // 1: azdext.PromptSubscriptionResponse @@ -1675,55 +1750,58 @@ var file_prompt_proto_goTypes = []any{ (*MultiSelectOptions)(nil), // 23: azdext.MultiSelectOptions (*PromptResourceOptions)(nil), // 24: azdext.PromptResourceOptions (*PromptResourceSelectOptions)(nil), // 25: azdext.PromptResourceSelectOptions - (*Subscription)(nil), // 26: azdext.Subscription - (*AzureContext)(nil), // 27: azdext.AzureContext - (*Location)(nil), // 28: azdext.Location - (*ResourceGroup)(nil), // 29: azdext.ResourceGroup - (*ResourceExtended)(nil), // 30: azdext.ResourceExtended + (*PromptResourceGroupOptions)(nil), // 26: azdext.PromptResourceGroupOptions + (*Subscription)(nil), // 27: azdext.Subscription + (*AzureContext)(nil), // 28: azdext.AzureContext + (*Location)(nil), // 29: azdext.Location + (*ResourceGroup)(nil), // 30: azdext.ResourceGroup + (*ResourceExtended)(nil), // 31: azdext.ResourceExtended } var file_prompt_proto_depIdxs = []int32{ - 26, // 0: azdext.PromptSubscriptionResponse.subscription:type_name -> azdext.Subscription - 27, // 1: azdext.PromptLocationRequest.azure_context:type_name -> azdext.AzureContext - 28, // 2: azdext.PromptLocationResponse.location:type_name -> azdext.Location - 27, // 3: azdext.PromptResourceGroupRequest.azure_context:type_name -> azdext.AzureContext - 29, // 4: azdext.PromptResourceGroupResponse.resource_group:type_name -> azdext.ResourceGroup - 18, // 5: azdext.ConfirmRequest.options:type_name -> azdext.ConfirmOptions - 19, // 6: azdext.PromptRequest.options:type_name -> azdext.PromptOptions - 22, // 7: azdext.SelectRequest.options:type_name -> azdext.SelectOptions - 23, // 8: azdext.MultiSelectRequest.options:type_name -> azdext.MultiSelectOptions - 21, // 9: azdext.MultiSelectResponse.values:type_name -> azdext.MultiSelectChoice - 27, // 10: azdext.PromptSubscriptionResourceRequest.azure_context:type_name -> azdext.AzureContext - 24, // 11: azdext.PromptSubscriptionResourceRequest.options:type_name -> azdext.PromptResourceOptions - 30, // 12: azdext.PromptSubscriptionResourceResponse.resource:type_name -> azdext.ResourceExtended - 27, // 13: azdext.PromptResourceGroupResourceRequest.azure_context:type_name -> azdext.AzureContext - 24, // 14: azdext.PromptResourceGroupResourceRequest.options:type_name -> azdext.PromptResourceOptions - 30, // 15: azdext.PromptResourceGroupResourceResponse.resource:type_name -> azdext.ResourceExtended - 20, // 16: azdext.SelectOptions.choices:type_name -> azdext.SelectChoice - 21, // 17: azdext.MultiSelectOptions.choices:type_name -> azdext.MultiSelectChoice - 25, // 18: azdext.PromptResourceOptions.select_options:type_name -> azdext.PromptResourceSelectOptions - 0, // 19: azdext.PromptService.PromptSubscription:input_type -> azdext.PromptSubscriptionRequest - 2, // 20: azdext.PromptService.PromptLocation:input_type -> azdext.PromptLocationRequest - 4, // 21: azdext.PromptService.PromptResourceGroup:input_type -> azdext.PromptResourceGroupRequest - 6, // 22: azdext.PromptService.Confirm:input_type -> azdext.ConfirmRequest - 8, // 23: azdext.PromptService.Prompt:input_type -> azdext.PromptRequest - 10, // 24: azdext.PromptService.Select:input_type -> azdext.SelectRequest - 12, // 25: azdext.PromptService.MultiSelect:input_type -> azdext.MultiSelectRequest - 14, // 26: azdext.PromptService.PromptSubscriptionResource:input_type -> azdext.PromptSubscriptionResourceRequest - 16, // 27: azdext.PromptService.PromptResourceGroupResource:input_type -> azdext.PromptResourceGroupResourceRequest - 1, // 28: azdext.PromptService.PromptSubscription:output_type -> azdext.PromptSubscriptionResponse - 3, // 29: azdext.PromptService.PromptLocation:output_type -> azdext.PromptLocationResponse - 5, // 30: azdext.PromptService.PromptResourceGroup:output_type -> azdext.PromptResourceGroupResponse - 7, // 31: azdext.PromptService.Confirm:output_type -> azdext.ConfirmResponse - 9, // 32: azdext.PromptService.Prompt:output_type -> azdext.PromptResponse - 11, // 33: azdext.PromptService.Select:output_type -> azdext.SelectResponse - 13, // 34: azdext.PromptService.MultiSelect:output_type -> azdext.MultiSelectResponse - 15, // 35: azdext.PromptService.PromptSubscriptionResource:output_type -> azdext.PromptSubscriptionResourceResponse - 17, // 36: azdext.PromptService.PromptResourceGroupResource:output_type -> azdext.PromptResourceGroupResourceResponse - 28, // [28:37] is the sub-list for method output_type - 19, // [19:28] is the sub-list for method input_type - 19, // [19:19] is the sub-list for extension type_name - 19, // [19:19] is the sub-list for extension extendee - 0, // [0:19] is the sub-list for field type_name + 27, // 0: azdext.PromptSubscriptionResponse.subscription:type_name -> azdext.Subscription + 28, // 1: azdext.PromptLocationRequest.azure_context:type_name -> azdext.AzureContext + 29, // 2: azdext.PromptLocationResponse.location:type_name -> azdext.Location + 28, // 3: azdext.PromptResourceGroupRequest.azure_context:type_name -> azdext.AzureContext + 26, // 4: azdext.PromptResourceGroupRequest.options:type_name -> azdext.PromptResourceGroupOptions + 30, // 5: azdext.PromptResourceGroupResponse.resource_group:type_name -> azdext.ResourceGroup + 18, // 6: azdext.ConfirmRequest.options:type_name -> azdext.ConfirmOptions + 19, // 7: azdext.PromptRequest.options:type_name -> azdext.PromptOptions + 22, // 8: azdext.SelectRequest.options:type_name -> azdext.SelectOptions + 23, // 9: azdext.MultiSelectRequest.options:type_name -> azdext.MultiSelectOptions + 21, // 10: azdext.MultiSelectResponse.values:type_name -> azdext.MultiSelectChoice + 28, // 11: azdext.PromptSubscriptionResourceRequest.azure_context:type_name -> azdext.AzureContext + 24, // 12: azdext.PromptSubscriptionResourceRequest.options:type_name -> azdext.PromptResourceOptions + 31, // 13: azdext.PromptSubscriptionResourceResponse.resource:type_name -> azdext.ResourceExtended + 28, // 14: azdext.PromptResourceGroupResourceRequest.azure_context:type_name -> azdext.AzureContext + 24, // 15: azdext.PromptResourceGroupResourceRequest.options:type_name -> azdext.PromptResourceOptions + 31, // 16: azdext.PromptResourceGroupResourceResponse.resource:type_name -> azdext.ResourceExtended + 20, // 17: azdext.SelectOptions.choices:type_name -> azdext.SelectChoice + 21, // 18: azdext.MultiSelectOptions.choices:type_name -> azdext.MultiSelectChoice + 25, // 19: azdext.PromptResourceOptions.select_options:type_name -> azdext.PromptResourceSelectOptions + 25, // 20: azdext.PromptResourceGroupOptions.select_options:type_name -> azdext.PromptResourceSelectOptions + 0, // 21: azdext.PromptService.PromptSubscription:input_type -> azdext.PromptSubscriptionRequest + 2, // 22: azdext.PromptService.PromptLocation:input_type -> azdext.PromptLocationRequest + 4, // 23: azdext.PromptService.PromptResourceGroup:input_type -> azdext.PromptResourceGroupRequest + 6, // 24: azdext.PromptService.Confirm:input_type -> azdext.ConfirmRequest + 8, // 25: azdext.PromptService.Prompt:input_type -> azdext.PromptRequest + 10, // 26: azdext.PromptService.Select:input_type -> azdext.SelectRequest + 12, // 27: azdext.PromptService.MultiSelect:input_type -> azdext.MultiSelectRequest + 14, // 28: azdext.PromptService.PromptSubscriptionResource:input_type -> azdext.PromptSubscriptionResourceRequest + 16, // 29: azdext.PromptService.PromptResourceGroupResource:input_type -> azdext.PromptResourceGroupResourceRequest + 1, // 30: azdext.PromptService.PromptSubscription:output_type -> azdext.PromptSubscriptionResponse + 3, // 31: azdext.PromptService.PromptLocation:output_type -> azdext.PromptLocationResponse + 5, // 32: azdext.PromptService.PromptResourceGroup:output_type -> azdext.PromptResourceGroupResponse + 7, // 33: azdext.PromptService.Confirm:output_type -> azdext.ConfirmResponse + 9, // 34: azdext.PromptService.Prompt:output_type -> azdext.PromptResponse + 11, // 35: azdext.PromptService.Select:output_type -> azdext.SelectResponse + 13, // 36: azdext.PromptService.MultiSelect:output_type -> azdext.MultiSelectResponse + 15, // 37: azdext.PromptService.PromptSubscriptionResource:output_type -> azdext.PromptSubscriptionResourceResponse + 17, // 38: azdext.PromptService.PromptResourceGroupResource:output_type -> azdext.PromptResourceGroupResourceResponse + 30, // [30:39] is the sub-list for method output_type + 21, // [21:30] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_prompt_proto_init() } @@ -1744,7 +1822,7 @@ func file_prompt_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_prompt_proto_rawDesc), len(file_prompt_proto_rawDesc)), NumEnums: 0, - NumMessages: 26, + NumMessages: 27, NumExtensions: 0, NumServices: 1, }, diff --git a/cli/azd/test/mocks/mockprompt/mock_prompt_service.go b/cli/azd/test/mocks/mockprompt/mock_prompt_service.go new file mode 100644 index 00000000000..bfd6157c359 --- /dev/null +++ b/cli/azd/test/mocks/mockprompt/mock_prompt_service.go @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package mockprompt + +import ( + "context" + + "github.com/azure/azure-dev/cli/azd/pkg/account" + "github.com/azure/azure-dev/cli/azd/pkg/azapi" + "github.com/azure/azure-dev/cli/azd/pkg/prompt" + "github.com/stretchr/testify/mock" +) + +type MockPromptService struct { + mock.Mock +} + +func (m *MockPromptService) PromptSubscription( + ctx context.Context, + selectorOptions *prompt.SelectOptions, +) (*account.Subscription, error) { + args := m.Called(ctx, selectorOptions) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*account.Subscription), args.Error(1) +} + +func (m *MockPromptService) PromptLocation( + ctx context.Context, + azureContext *prompt.AzureContext, + selectorOptions *prompt.SelectOptions, +) (*account.Location, error) { + args := m.Called(ctx, azureContext, selectorOptions) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*account.Location), args.Error(1) +} + +func (m *MockPromptService) PromptResourceGroup( + ctx context.Context, + azureContext *prompt.AzureContext, + options *prompt.ResourceGroupOptions, +) (*azapi.ResourceGroup, error) { + args := m.Called(ctx, azureContext, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*azapi.ResourceGroup), args.Error(1) +} + +func (m *MockPromptService) PromptSubscriptionResource( + ctx context.Context, + azureContext *prompt.AzureContext, + options prompt.ResourceOptions, +) (*azapi.ResourceExtended, error) { + args := m.Called(ctx, azureContext, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*azapi.ResourceExtended), args.Error(1) +} + +func (m *MockPromptService) PromptResourceGroupResource( + ctx context.Context, + azureContext *prompt.AzureContext, + options prompt.ResourceOptions, +) (*azapi.ResourceExtended, error) { + args := m.Called(ctx, azureContext, options) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(*azapi.ResourceExtended), args.Error(1) +}