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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/azd/internal/grpcserver/account_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import (

"github.com/azure/azure-dev/cli/azd/pkg/account"
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type accountService struct {
Expand Down Expand Up @@ -63,6 +65,10 @@ func (s *accountService) LookupTenant(
ctx context.Context,
req *azdext.LookupTenantRequest,
) (*azdext.LookupTenantResponse, error) {
if req.SubscriptionId == "" {
return nil, status.Error(codes.InvalidArgument, "subscription id is required")
}

tenantId, err := s.subscriptionsManager.LookupTenant(ctx, req.SubscriptionId)
if err != nil {
return nil, err
Expand Down
2 changes: 1 addition & 1 deletion cli/azd/internal/grpcserver/compose_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ func (c *composeService) GetResourceType(
context.Context,
*azdext.GetResourceTypeRequest,
) (*azdext.GetResourceTypeResponse, error) {
panic("unimplemented")
return nil, status.Error(codes.Unimplemented, "GetResourceType is not yet implemented")
}

// ListResourceTypes lists all available resource types.
Expand Down
26 changes: 26 additions & 0 deletions cli/azd/internal/grpcserver/compose_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import (
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/azure/azure-dev/cli/azd/test/mocks/mockenv"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

func Test_ComposeService_AddResource(t *testing.T) {
Expand Down Expand Up @@ -229,3 +231,27 @@ func Test_Test_ComposeService_ListResourceTypes(t *testing.T) {
require.NotEmpty(t, randomResource.DisplayName)
require.NotEmpty(t, randomResource.Type)
}

func Test_ComposeService_GetResourceType_Unimplemented(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
lazyAzdContext := lazy.NewLazy(func() (*azdcontext.AzdContext, error) {
return nil, azdcontext.ErrNoProject
})
env := environment.New("test")
envManager := &mockenv.MockEnvManager{}
lazyEnvManager := lazy.NewLazy(func() (environment.Manager, error) {
return envManager, nil
})
lazyEnv := lazy.NewLazy(func() (*environment.Environment, error) {
return env, nil
})
service := NewComposeService(lazyAzdContext, lazyEnv, lazyEnvManager)

_, err := service.GetResourceType(*mockContext.Context, &azdext.GetResourceTypeRequest{})
require.Error(t, err)

st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, codes.Unimplemented, st.Code())
require.Contains(t, st.Message(), "not yet implemented")
}
23 changes: 20 additions & 3 deletions cli/azd/internal/grpcserver/container_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/input"
"github.com/azure/azure-dev/cli/azd/pkg/lazy"
"github.com/azure/azure-dev/cli/azd/pkg/project"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type containerService struct {
Expand Down Expand Up @@ -45,14 +47,19 @@ func (c *containerService) Build(
ctx context.Context,
req *azdext.ContainerBuildRequest,
) (*azdext.ContainerBuildResponse, error) {
if req.ServiceName == "" {
return nil, status.Error(codes.InvalidArgument, "service name is required")
}

projectConfig, err := c.lazyProject.GetValue()
if err != nil {
return nil, err
}

serviceConfig, has := projectConfig.Services[req.ServiceName]
if !has {
return nil, fmt.Errorf("service %q not found in project configuration", req.ServiceName)
return nil, status.Errorf(codes.NotFound,
"service %q not found in project configuration", req.ServiceName)
}

containerHelper, err := c.lazyContainerHelper.GetValue()
Expand Down Expand Up @@ -95,14 +102,19 @@ func (c *containerService) Package(
ctx context.Context,
req *azdext.ContainerPackageRequest,
) (*azdext.ContainerPackageResponse, error) {
if req.ServiceName == "" {
return nil, status.Error(codes.InvalidArgument, "service name is required")
}

projectConfig, err := c.lazyProject.GetValue()
if err != nil {
return nil, err
}

serviceConfig, has := projectConfig.Services[req.ServiceName]
if !has {
return nil, fmt.Errorf("service %q not found in project configuration", req.ServiceName)
return nil, status.Errorf(codes.NotFound,
"service %q not found in project configuration", req.ServiceName)
}

containerHelper, err := c.lazyContainerHelper.GetValue()
Expand Down Expand Up @@ -145,14 +157,19 @@ func (c *containerService) Publish(
ctx context.Context,
req *azdext.ContainerPublishRequest,
) (*azdext.ContainerPublishResponse, error) {
if req.ServiceName == "" {
return nil, status.Error(codes.InvalidArgument, "service name is required")
}

projectConfig, err := c.lazyProject.GetValue()
if err != nil {
return nil, err
}

serviceConfig, has := projectConfig.Services[req.ServiceName]
if !has {
return nil, fmt.Errorf("service %q not found in project configuration", req.ServiceName)
return nil, status.Errorf(codes.NotFound,
"service %q not found in project configuration", req.ServiceName)
}

containerHelper, err := c.lazyContainerHelper.GetValue()
Expand Down
22 changes: 19 additions & 3 deletions cli/azd/internal/grpcserver/copilot_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -359,6 +359,13 @@ func convertFileChangeType(ct watch.FileChangeType) azdext.CopilotFileChangeType
// convertSessionEvent converts a Copilot SDK SessionEvent to the proto representation.
// Event data is marshaled to JSON then converted to google.protobuf.Struct for
// dynamic, schema-free transport.
//
// Errors during data conversion are intentionally logged and swallowed rather than propagated.
// This function feeds into a streaming response, and its signature intentionally omits an error
// return to support graceful degradation: callers always receive a valid event with at least
// the Type and Timestamp fields populated, even when the Data payload cannot be converted.
// Failing the entire stream for a single malformed event would be worse than delivering
// partial data.
func convertSessionEvent(event agent.SessionEvent) *azdext.CopilotSessionEvent {
protoEvent := &azdext.CopilotSessionEvent{
Type: string(event.Type),
Expand All @@ -368,19 +375,28 @@ func convertSessionEvent(event agent.SessionEvent) *azdext.CopilotSessionEvent {
// Marshal event.Data to JSON, then to protobuf Struct
jsonBytes, err := json.Marshal(event.Data)
if err != nil {
log.Printf("[copilot-service] failed to marshal event data: %v", err)
log.Printf(
"[copilot-service] failed to marshal event data for event type %q: %v",
event.Type, err,
)
return protoEvent
}

var dataMap map[string]any
if err := json.Unmarshal(jsonBytes, &dataMap); err != nil {
log.Printf("[copilot-service] failed to unmarshal event data to map: %v", err)
log.Printf(
"[copilot-service] failed to unmarshal event data to map for event type %q: %v",
event.Type, err,
)
return protoEvent
}

protoStruct, err := structpb.NewStruct(dataMap)
if err != nil {
log.Printf("[copilot-service] failed to create protobuf struct: %v", err)
log.Printf(
"[copilot-service] failed to create protobuf struct for event type %q: %v",
event.Type, err,
)
return protoEvent
}

Expand Down
10 changes: 10 additions & 0 deletions cli/azd/internal/grpcserver/environment_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/environment"
"github.com/azure/azure-dev/cli/azd/pkg/environment/azdcontext"
"github.com/azure/azure-dev/cli/azd/pkg/lazy"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

type environmentService struct {
Expand Down Expand Up @@ -152,6 +154,10 @@ func (s *environmentService) GetValues(

// GetValue retrieves the value of a specific key in the specified environment.
func (s *environmentService) GetValue(ctx context.Context, req *azdext.GetEnvRequest) (*azdext.KeyValueResponse, error) {
if req.Key == "" {
return nil, status.Error(codes.InvalidArgument, "key is required")
}

env, err := s.resolveEnvironment(ctx, req.EnvName)
if err != nil {
return nil, err
Expand All @@ -167,6 +173,10 @@ func (s *environmentService) GetValue(ctx context.Context, req *azdext.GetEnvReq

// SetValue sets the value of a key in the specified environment.
func (s *environmentService) SetValue(ctx context.Context, req *azdext.SetEnvRequest) (*azdext.EmptyResponse, error) {
if req.Key == "" {
return nil, status.Error(codes.InvalidArgument, "key is required")
}

envManager, err := s.lazyEnvManager.GetValue()
if err != nil {
return nil, err
Expand Down
59 changes: 59 additions & 0 deletions cli/azd/internal/grpcserver/environment_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"github.com/azure/azure-dev/cli/azd/pkg/project"
"github.com/azure/azure-dev/cli/azd/test/mocks"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)

// Test_EnvironmentService_NoEnvironment verifies that when no environments are set,
Expand Down Expand Up @@ -332,3 +334,60 @@ func Test_EnvironmentService_ResolveEnvironment(t *testing.T) {
})
})
}

// Test_EnvironmentService_EmptyKeyValidation verifies that GetValue and SetValue
// return InvalidArgument when called with an empty key.
func Test_EnvironmentService_EmptyKeyValidation(t *testing.T) {
mockContext := mocks.NewMockContext(context.Background())
temp := t.TempDir()

azdContext := azdcontext.NewAzdContextWithDirectory(temp)
projectConfig := project.ProjectConfig{Name: "test"}
err := project.Save(*mockContext.Context, &projectConfig, azdContext.ProjectPath())
require.NoError(t, err)

fileConfigManager := config.NewFileConfigManager(config.NewManager())
localDataStore := environment.NewLocalFileDataStore(azdContext, fileConfigManager)
envManager, err := environment.NewManager(
mockContext.Container, azdContext, mockContext.Console, localDataStore, nil,
)
require.NoError(t, err)

env1, err := envManager.Create(*mockContext.Context, environment.Spec{Name: "env1"})
require.NoError(t, err)
require.NoError(t, envManager.Save(*mockContext.Context, env1))
require.NoError(t, azdContext.SetProjectState(
azdcontext.ProjectState{DefaultEnvironment: "env1"},
))

service := NewEnvironmentService(lazy.From(azdContext), lazy.From(envManager))
ctx := *mockContext.Context

tests := []struct {
name string
method string
}{
{"GetValue_empty_key", "GetValue"},
{"SetValue_empty_key", "SetValue"},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
var callErr error
switch tt.method {
case "GetValue":
_, callErr = service.GetValue(ctx, &azdext.GetEnvRequest{Key: ""})
case "SetValue":
_, callErr = service.SetValue(
ctx, &azdext.SetEnvRequest{Key: "", Value: "v"},
)
}

require.Error(t, callErr)
st, ok := status.FromError(callErr)
require.True(t, ok)
require.Equal(t, codes.InvalidArgument, st.Code())
require.Contains(t, st.Message(), "key is required")
})
}
}
27 changes: 23 additions & 4 deletions cli/azd/internal/grpcserver/event_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,22 @@ func (s *eventService) onSubscribeProjectEvent(
subscribeMsg *azdext.SubscribeProjectEvent,
broker *grpcbroker.MessageBroker[azdext.EventMessage],
) error {
if subscribeMsg == nil || len(subscribeMsg.EventNames) == 0 {
return status.Error(codes.InvalidArgument, "event names are required")
}

projectConfig, err := s.lazyProject.GetValue()
if err != nil {
return err
}

for i := 0; i < len(subscribeMsg.EventNames); i++ {
eventName := subscribeMsg.EventNames[i]
for i, eventName := range subscribeMsg.EventNames {
if eventName == "" {
return status.Errorf(
codes.InvalidArgument,
"event name at index %d cannot be empty", i,
)
}

evt := ext.Event(eventName)
// Pass the stream context (ctx) which has extension claims
Expand Down Expand Up @@ -191,13 +200,23 @@ func (s *eventService) onSubscribeServiceEvent(
subscribeMsg *azdext.SubscribeServiceEvent,
broker *grpcbroker.MessageBroker[azdext.EventMessage],
) error {
if subscribeMsg == nil || len(subscribeMsg.EventNames) == 0 {
return status.Error(codes.InvalidArgument, "event names are required")
}

projectConfig, err := s.lazyProject.GetValue()
if err != nil {
return err
}

for i := 0; i < len(subscribeMsg.EventNames); i++ {
eventName := subscribeMsg.EventNames[i]
for i, eventName := range subscribeMsg.EventNames {
if eventName == "" {
return status.Errorf(
codes.InvalidArgument,
"event name at index %d cannot be empty", i,
)
}

evt := ext.Event(eventName)
for _, serviceConfig := range projectConfig.Services {
if subscribeMsg.Language != "" && string(serviceConfig.Language) != subscribeMsg.Language {
Expand Down
38 changes: 37 additions & 1 deletion cli/azd/internal/grpcserver/event_service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)

// MockBidiStreamingServer mocks the gRPC bidirectional streaming server using generics
Expand Down Expand Up @@ -178,7 +180,7 @@ func TestEventService_handleSubscribeProjectEvent(t *testing.T) {
subscribeMsg: &azdext.SubscribeProjectEvent{
EventNames: []string{},
},
expectError: false,
expectError: true,
},
}

Expand Down Expand Up @@ -360,3 +362,37 @@ func TestEventService_New(t *testing.T) {
assert.NotNil(t, eventSvc.lazyEnv)
assert.NotNil(t, eventSvc.console)
}

func TestEventService_EmptyEventNameInArray(t *testing.T) {
service, _ := createTestEventService()
extension := createTestExtension()
ctx := t.Context()

t.Run("project_event_with_empty_name", func(t *testing.T) {
var mockBroker *grpcbroker.MessageBroker[azdext.EventMessage]

err := service.onSubscribeProjectEvent(ctx, extension, &azdext.SubscribeProjectEvent{
EventNames: []string{"prepackage", "", "postpackage"},
}, mockBroker)

require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, codes.InvalidArgument, st.Code())
require.Contains(t, st.Message(), "event name at index 1 cannot be empty")
})

t.Run("service_event_with_empty_name", func(t *testing.T) {
var mockBroker *grpcbroker.MessageBroker[azdext.EventMessage]

err := service.onSubscribeServiceEvent(ctx, extension, &azdext.SubscribeServiceEvent{
EventNames: []string{"", "prepackage"},
}, mockBroker)

require.Error(t, err)
st, ok := status.FromError(err)
require.True(t, ok)
require.Equal(t, codes.InvalidArgument, st.Code())
require.Contains(t, st.Message(), "event name at index 0 cannot be empty")
})
}
Loading
Loading