Skip to content

Commit 9727cb7

Browse files
committed
feat: migrate UpdateProjectResource API to Connect RPC
1 parent 404236f commit 9727cb7

File tree

2 files changed

+321
-3
lines changed

2 files changed

+321
-3
lines changed

internal/api/v1beta1connect/resource.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,68 @@ func (h *ConnectHandler) GetProjectResource(ctx context.Context, request *connec
147147
}), nil
148148
}
149149

150+
func (h *ConnectHandler) UpdateProjectResource(ctx context.Context, request *connect.Request[frontierv1beta1.UpdateProjectResourceRequest]) (*connect.Response[frontierv1beta1.UpdateProjectResourceResponse], error) {
151+
if request.Msg.GetBody() == nil {
152+
return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest)
153+
}
154+
155+
var metaDataMap metadata.Metadata
156+
var err error
157+
if request.Msg.GetBody().GetMetadata() != nil {
158+
metaDataMap = metadata.Build(request.Msg.GetBody().GetMetadata().AsMap())
159+
}
160+
161+
parentProject, err := h.projectService.Get(ctx, request.Msg.GetProjectId())
162+
if err != nil {
163+
return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError)
164+
}
165+
166+
principalType := schema.UserPrincipal
167+
principalID := request.Msg.GetBody().GetPrincipal()
168+
if ns, id, err := schema.SplitNamespaceAndResourceID(request.Msg.GetBody().GetPrincipal()); err == nil {
169+
principalType = ns
170+
principalID = id
171+
}
172+
namespaceID := schema.ParseNamespaceAliasIfRequired(request.Msg.GetBody().GetNamespace())
173+
updatedResource, err := h.resourceService.Update(ctx, resource.Resource{
174+
ID: request.Msg.GetId(),
175+
ProjectID: parentProject.ID,
176+
NamespaceID: namespaceID,
177+
Name: request.Msg.GetBody().GetName(),
178+
PrincipalID: principalID,
179+
PrincipalType: principalType,
180+
Metadata: metaDataMap,
181+
})
182+
if err != nil {
183+
switch {
184+
case errors.Is(err, resource.ErrNotExist),
185+
errors.Is(err, resource.ErrInvalidUUID),
186+
errors.Is(err, resource.ErrInvalidID):
187+
return nil, connect.NewError(connect.CodeNotFound, ErrResourceNotFound)
188+
case errors.Is(err, resource.ErrInvalidDetail),
189+
errors.Is(err, resource.ErrInvalidURN):
190+
return nil, connect.NewError(connect.CodeInvalidArgument, ErrBadRequest)
191+
case errors.Is(err, resource.ErrConflict):
192+
return nil, connect.NewError(connect.CodeAlreadyExists, ErrConflictRequest)
193+
default:
194+
return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError)
195+
}
196+
}
197+
198+
resourcePB, err := transformResourceToPB(updatedResource)
199+
if err != nil {
200+
return nil, connect.NewError(connect.CodeInternal, ErrInternalServerError)
201+
}
202+
203+
audit.GetAuditor(ctx, parentProject.Organization.ID).Log(audit.ResourceUpdatedEvent, audit.Target{
204+
ID: updatedResource.ID,
205+
Type: updatedResource.NamespaceID,
206+
})
207+
return connect.NewResponse(&frontierv1beta1.UpdateProjectResourceResponse{
208+
Resource: resourcePB,
209+
}), nil
210+
}
211+
150212
func transformResourceToPB(from resource.Resource) (*frontierv1beta1.Resource, error) {
151213
var metadata *structpb.Struct
152214
var err error

internal/api/v1beta1connect/resource_test.go

Lines changed: 259 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ func TestConnectHandler_GetProjectResource(t *testing.T) {
395395
},
396396
request: connect.NewRequest(&frontierv1beta1.GetProjectResourceRequest{}),
397397
want: nil,
398-
wantErr: connect.NewError(connect.CodeNotFound, ErrNotFound),
398+
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
399399
},
400400
{
401401
name: "should return not found error if id is not uuid",
@@ -406,7 +406,7 @@ func TestConnectHandler_GetProjectResource(t *testing.T) {
406406
Id: "some-id",
407407
}),
408408
want: nil,
409-
wantErr: connect.NewError(connect.CodeNotFound, ErrNotFound),
409+
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
410410
},
411411
{
412412
name: "should return not found error if resource not exist",
@@ -417,7 +417,7 @@ func TestConnectHandler_GetProjectResource(t *testing.T) {
417417
Id: testResource.ID,
418418
}),
419419
want: nil,
420-
wantErr: connect.NewError(connect.CodeNotFound, ErrNotFound),
420+
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
421421
},
422422
{
423423
name: "should return success if resource service returns resource",
@@ -468,3 +468,259 @@ func TestConnectHandler_GetProjectResource(t *testing.T) {
468468
})
469469
}
470470
}
471+
472+
func TestConnectHandler_UpdateProjectResource(t *testing.T) {
473+
tests := []struct {
474+
name string
475+
setup func(rs *mocks.ResourceService, ps *mocks.ProjectService)
476+
request *connect.Request[frontierv1beta1.UpdateProjectResourceRequest]
477+
want *connect.Response[frontierv1beta1.UpdateProjectResourceResponse]
478+
wantErr error
479+
}{
480+
{
481+
name: "should return error if request body is nil",
482+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
483+
ProjectId: testProjectID,
484+
Body: nil,
485+
}),
486+
want: nil,
487+
wantErr: connect.NewError(connect.CodeInvalidArgument, ErrBadRequest),
488+
},
489+
{
490+
name: "should return internal error if project service returns error",
491+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
492+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{}, errors.New("test error"))
493+
},
494+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
495+
Id: testResourceID,
496+
ProjectId: testResource.ProjectID,
497+
Body: &frontierv1beta1.ResourceRequestBody{
498+
Name: testResource.Name,
499+
Namespace: testResource.NamespaceID,
500+
Principal: testUserID,
501+
},
502+
}),
503+
want: nil,
504+
wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError),
505+
},
506+
{
507+
name: "should return internal error if resource service returns error",
508+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
509+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
510+
ID: testResource.ProjectID,
511+
}, nil)
512+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), resource.Resource{
513+
ID: testResourceID,
514+
Name: testResource.Name,
515+
ProjectID: testResource.ProjectID,
516+
PrincipalID: testResource.PrincipalID,
517+
PrincipalType: testResource.PrincipalType,
518+
NamespaceID: testResource.NamespaceID,
519+
}).Return(resource.Resource{}, errors.New("test error"))
520+
},
521+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
522+
Id: testResourceID,
523+
ProjectId: testResource.ProjectID,
524+
Body: &frontierv1beta1.ResourceRequestBody{
525+
Name: testResource.Name,
526+
Namespace: testResource.NamespaceID,
527+
Principal: testUserID,
528+
},
529+
}),
530+
want: nil,
531+
wantErr: connect.NewError(connect.CodeInternal, ErrInternalServerError),
532+
},
533+
{
534+
name: "should return not found error if resource not exist",
535+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
536+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
537+
ID: testResource.ProjectID,
538+
}, nil)
539+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), resource.Resource{
540+
ID: testResourceID,
541+
Name: testResource.Name,
542+
ProjectID: testResource.ProjectID,
543+
NamespaceID: testResource.NamespaceID,
544+
PrincipalID: testResource.PrincipalID,
545+
PrincipalType: testResource.PrincipalType,
546+
}).Return(resource.Resource{}, resource.ErrNotExist)
547+
},
548+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
549+
Id: testResourceID,
550+
ProjectId: testResource.ProjectID,
551+
Body: &frontierv1beta1.ResourceRequestBody{
552+
Name: testResource.Name,
553+
Namespace: testResource.NamespaceID,
554+
Principal: testUserID,
555+
},
556+
}),
557+
want: nil,
558+
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
559+
},
560+
{
561+
name: "should return not found error if id is invalid",
562+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
563+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
564+
ID: testResource.ProjectID,
565+
}, nil)
566+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), resource.Resource{
567+
ID: "some-id",
568+
Name: testResource.Name,
569+
ProjectID: testResource.ProjectID,
570+
PrincipalID: testResource.PrincipalID,
571+
NamespaceID: testResource.NamespaceID,
572+
PrincipalType: testResource.PrincipalType,
573+
}).Return(resource.Resource{}, resource.ErrInvalidUUID)
574+
},
575+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
576+
Id: "some-id",
577+
ProjectId: testResource.ProjectID,
578+
Body: &frontierv1beta1.ResourceRequestBody{
579+
Name: testResource.Name,
580+
Namespace: testResource.NamespaceID,
581+
Principal: testUserID,
582+
},
583+
}),
584+
want: nil,
585+
wantErr: connect.NewError(connect.CodeNotFound, ErrResourceNotFound),
586+
},
587+
{
588+
name: "should return bad request error if field value is invalid",
589+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
590+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
591+
ID: testResource.ProjectID,
592+
}, nil)
593+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), resource.Resource{
594+
ID: testResourceID,
595+
Name: testResource.Name,
596+
ProjectID: testResource.ProjectID,
597+
PrincipalID: testResource.PrincipalID,
598+
NamespaceID: testResource.NamespaceID,
599+
PrincipalType: testResource.PrincipalType,
600+
}).Return(resource.Resource{}, resource.ErrInvalidDetail)
601+
},
602+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
603+
Id: testResourceID,
604+
ProjectId: testResource.ProjectID,
605+
Body: &frontierv1beta1.ResourceRequestBody{
606+
Name: testResource.Name,
607+
Namespace: testResource.NamespaceID,
608+
Principal: testUserID,
609+
},
610+
}),
611+
want: nil,
612+
wantErr: connect.NewError(connect.CodeInvalidArgument, ErrBadRequest),
613+
},
614+
{
615+
name: "should return conflict error if resource service returns conflict",
616+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
617+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
618+
ID: testResource.ProjectID,
619+
}, nil)
620+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), resource.Resource{
621+
ID: testResourceID,
622+
Name: testResource.Name,
623+
ProjectID: testResource.ProjectID,
624+
PrincipalID: testResource.PrincipalID,
625+
PrincipalType: testResource.PrincipalType,
626+
NamespaceID: testResource.NamespaceID,
627+
}).Return(resource.Resource{}, resource.ErrConflict)
628+
},
629+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
630+
Id: testResourceID,
631+
ProjectId: testResource.ProjectID,
632+
Body: &frontierv1beta1.ResourceRequestBody{
633+
Name: testResource.Name,
634+
Namespace: testResource.NamespaceID,
635+
Principal: testUserID,
636+
},
637+
}),
638+
want: nil,
639+
wantErr: connect.NewError(connect.CodeAlreadyExists, ErrConflictRequest),
640+
},
641+
{
642+
name: "should return success if resource service returns updated resource",
643+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
644+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
645+
ID: testResource.ProjectID,
646+
Organization: organization.Organization{
647+
ID: "test-org-id",
648+
},
649+
}, nil)
650+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), resource.Resource{
651+
ID: testResourceID,
652+
Name: testResource.Name,
653+
ProjectID: testResource.ProjectID,
654+
NamespaceID: testResource.NamespaceID,
655+
PrincipalID: testResource.PrincipalID,
656+
PrincipalType: testResource.PrincipalType,
657+
}).Return(testResource, nil)
658+
},
659+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
660+
Id: testResourceID,
661+
ProjectId: testResource.ProjectID,
662+
Body: &frontierv1beta1.ResourceRequestBody{
663+
Name: testResource.Name,
664+
Namespace: testResource.NamespaceID,
665+
Principal: testUserID,
666+
},
667+
}),
668+
want: connect.NewResponse(&frontierv1beta1.UpdateProjectResourceResponse{
669+
Resource: testResourcePB,
670+
}),
671+
wantErr: nil,
672+
},
673+
{
674+
name: "should handle metadata correctly",
675+
setup: func(rs *mocks.ResourceService, ps *mocks.ProjectService) {
676+
ps.EXPECT().Get(mock.AnythingOfType("context.backgroundCtx"), testResource.ProjectID).Return(project.Project{
677+
ID: testResource.ProjectID,
678+
Organization: organization.Organization{
679+
ID: "test-org-id",
680+
},
681+
}, nil)
682+
rs.EXPECT().Update(mock.AnythingOfType("context.backgroundCtx"), mock.MatchedBy(func(res resource.Resource) bool {
683+
return res.Name == testResource.Name && len(res.Metadata) > 0
684+
})).Return(testResource, nil)
685+
},
686+
request: connect.NewRequest(&frontierv1beta1.UpdateProjectResourceRequest{
687+
Id: testResourceID,
688+
ProjectId: testResource.ProjectID,
689+
Body: &frontierv1beta1.ResourceRequestBody{
690+
Name: testResource.Name,
691+
Namespace: testResource.NamespaceID,
692+
Principal: testUserID,
693+
Metadata: &structpb.Struct{
694+
Fields: map[string]*structpb.Value{
695+
"updated": structpb.NewStringValue("value"),
696+
},
697+
},
698+
},
699+
}),
700+
want: connect.NewResponse(&frontierv1beta1.UpdateProjectResourceResponse{
701+
Resource: testResourcePB,
702+
}),
703+
wantErr: nil,
704+
},
705+
}
706+
707+
for _, tt := range tests {
708+
t.Run(tt.name, func(t *testing.T) {
709+
mockResourceSrv := new(mocks.ResourceService)
710+
mockProjectSrv := new(mocks.ProjectService)
711+
if tt.setup != nil {
712+
tt.setup(mockResourceSrv, mockProjectSrv)
713+
}
714+
h := ConnectHandler{resourceService: mockResourceSrv, projectService: mockProjectSrv}
715+
resp, err := h.UpdateProjectResource(context.Background(), tt.request)
716+
if tt.wantErr != nil {
717+
assert.Error(t, err)
718+
assert.Equal(t, tt.wantErr.(*connect.Error).Code(), err.(*connect.Error).Code())
719+
assert.Equal(t, tt.wantErr.(*connect.Error).Message(), err.(*connect.Error).Message())
720+
} else {
721+
assert.NoError(t, err)
722+
assert.EqualValues(t, tt.want, resp)
723+
}
724+
})
725+
}
726+
}

0 commit comments

Comments
 (0)