Skip to content

Commit 2e004e1

Browse files
authored
Add unit tests for ListInvitations and ResolveInvitation (#3790)
Fix #3669
1 parent f31b931 commit 2e004e1

File tree

1 file changed

+262
-0
lines changed

1 file changed

+262
-0
lines changed

internal/controlplane/handlers_user_test.go

+262
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,16 @@ import (
3333
"google.golang.org/grpc/metadata"
3434

3535
mockdb "github.com/stacklok/minder/database/mock"
36+
"github.com/stacklok/minder/internal/auth"
3637
"github.com/stacklok/minder/internal/auth/jwt"
3738
mockjwt "github.com/stacklok/minder/internal/auth/jwt/mock"
39+
mockidentity "github.com/stacklok/minder/internal/auth/mock"
3840
"github.com/stacklok/minder/internal/authz/mock"
3941
serverconfig "github.com/stacklok/minder/internal/config/server"
4042
mockcrypto "github.com/stacklok/minder/internal/crypto/mock"
4143
"github.com/stacklok/minder/internal/db"
4244
"github.com/stacklok/minder/internal/events"
45+
"github.com/stacklok/minder/internal/flags"
4346
"github.com/stacklok/minder/internal/marketplaces"
4447
"github.com/stacklok/minder/internal/projects"
4548
"github.com/stacklok/minder/internal/providers"
@@ -479,3 +482,262 @@ func TestDeleteUser_gRPC(t *testing.T) {
479482
})
480483
}
481484
}
485+
486+
func TestListInvitations(t *testing.T) {
487+
t.Parallel()
488+
489+
userEmail := "[email protected]"
490+
project := uuid.New()
491+
code := "code"
492+
identitySubject := "subject1"
493+
role := "viewer"
494+
projectDisplayName := "Project"
495+
projectMetadata, err := json.Marshal(
496+
projects.Metadata{Public: projects.PublicMetadataV1{DisplayName: projectDisplayName}},
497+
)
498+
require.NoError(t, err)
499+
500+
testCases := []struct {
501+
name string
502+
setup func(store *mockdb.MockStore, idClient *mockidentity.MockResolver)
503+
expectedError string
504+
expectedResult *pb.ListInvitationsResponse
505+
}{
506+
{
507+
name: "Success",
508+
setup: func(store *mockdb.MockStore, idClient *mockidentity.MockResolver) {
509+
store.EXPECT().GetInvitationsByEmail(gomock.Any(), userEmail).Return([]db.GetInvitationsByEmailRow{
510+
{
511+
Project: project,
512+
Code: code,
513+
IdentitySubject: identitySubject,
514+
Email: userEmail,
515+
Role: role,
516+
},
517+
}, nil)
518+
idClient.EXPECT().Resolve(gomock.Any(), identitySubject).Return(&auth.Identity{
519+
UserID: identitySubject,
520+
HumanName: "User",
521+
}, nil)
522+
store.EXPECT().GetProjectByID(gomock.Any(), project).Return(db.Project{
523+
Name: "project1",
524+
Metadata: projectMetadata,
525+
}, nil)
526+
},
527+
expectedResult: &pb.ListInvitationsResponse{
528+
Invitations: []*pb.Invitation{
529+
{
530+
Project: project.String(),
531+
ProjectDisplay: projectDisplayName,
532+
Code: code,
533+
Role: role,
534+
Email: userEmail,
535+
Sponsor: identitySubject,
536+
},
537+
},
538+
},
539+
},
540+
}
541+
542+
for _, tc := range testCases {
543+
t.Run(tc.name, func(t *testing.T) {
544+
t.Parallel()
545+
546+
ctrl := gomock.NewController(t)
547+
defer ctrl.Finish()
548+
549+
user := openid.New()
550+
assert.NoError(t, user.Set("email", userEmail))
551+
552+
ctx := context.Background()
553+
ctx = jwt.WithAuthTokenContext(ctx, user)
554+
555+
mockStore := mockdb.NewMockStore(ctrl)
556+
mockIdClient := mockidentity.NewMockResolver(ctrl)
557+
if tc.setup != nil {
558+
tc.setup(mockStore, mockIdClient)
559+
}
560+
561+
featureClient := &flags.FakeClient{}
562+
featureClient.Data = map[string]any{
563+
"user_management": true,
564+
}
565+
566+
server := &Server{
567+
store: mockStore,
568+
idClient: mockIdClient,
569+
featureFlags: featureClient,
570+
}
571+
572+
response, err := server.ListInvitations(ctx, &pb.ListInvitationsRequest{})
573+
574+
if tc.expectedError != "" {
575+
require.Error(t, err)
576+
require.Contains(t, err.Error(), tc.expectedError)
577+
return
578+
}
579+
580+
require.NoError(t, err)
581+
require.Equal(t, len(tc.expectedResult.Invitations), len(response.Invitations))
582+
require.Equal(t, tc.expectedResult.Invitations[0].Email, response.Invitations[0].Email)
583+
require.Equal(t, tc.expectedResult.Invitations[0].Project, response.Invitations[0].Project)
584+
require.Equal(t, tc.expectedResult.Invitations[0].ProjectDisplay, response.Invitations[0].ProjectDisplay)
585+
require.Equal(t, tc.expectedResult.Invitations[0].Code, response.Invitations[0].Code)
586+
require.Equal(t, tc.expectedResult.Invitations[0].Role, response.Invitations[0].Role)
587+
require.Equal(t, tc.expectedResult.Invitations[0].Sponsor, response.Invitations[0].Sponsor)
588+
})
589+
}
590+
}
591+
592+
func TestResolveInvitation(t *testing.T) {
593+
t.Parallel()
594+
595+
userEmail := "[email protected]"
596+
userSubject := "subject1"
597+
projectDisplayName := "Project"
598+
projectMetadata, err := json.Marshal(
599+
projects.Metadata{Public: projects.PublicMetadataV1{DisplayName: projectDisplayName}},
600+
)
601+
require.NoError(t, err)
602+
603+
testCases := []struct {
604+
name string
605+
accept bool
606+
setup func(store *mockdb.MockStore)
607+
expectedError string
608+
}{
609+
{
610+
name: "code not found",
611+
setup: func(store *mockdb.MockStore) {
612+
store.EXPECT().GetInvitationByCode(gomock.Any(), gomock.Any()).Return(db.GetInvitationByCodeRow{}, sql.ErrNoRows)
613+
},
614+
expectedError: "invitation not found or already used",
615+
},
616+
{
617+
name: "user self resolving",
618+
setup: func(store *mockdb.MockStore) {
619+
userId := int32(1)
620+
store.EXPECT().GetInvitationByCode(gomock.Any(), gomock.Any()).Return(db.GetInvitationByCodeRow{
621+
Project: projectID,
622+
IdentitySubject: userSubject,
623+
Sponsor: userId,
624+
}, nil)
625+
store.EXPECT().GetUserBySubject(gomock.Any(), userSubject).Return(db.User{
626+
ID: userId,
627+
}, nil)
628+
},
629+
expectedError: "user cannot resolve their own invitation",
630+
},
631+
{
632+
name: "expired invitation",
633+
setup: func(store *mockdb.MockStore) {
634+
store.EXPECT().GetInvitationByCode(gomock.Any(), gomock.Any()).Return(db.GetInvitationByCodeRow{
635+
Project: projectID,
636+
IdentitySubject: userSubject,
637+
Sponsor: 1,
638+
UpdatedAt: time.Now().Add(-time.Hour * 24 * 8), // updated 8 days ago
639+
}, nil)
640+
store.EXPECT().GetUserBySubject(gomock.Any(), userSubject).Return(db.User{
641+
ID: 2,
642+
}, nil)
643+
},
644+
expectedError: "invitation expired",
645+
},
646+
{
647+
name: "Success accept",
648+
accept: true,
649+
setup: func(store *mockdb.MockStore) {
650+
store.EXPECT().GetInvitationByCode(gomock.Any(), gomock.Any()).Return(db.GetInvitationByCodeRow{
651+
Project: projectID,
652+
IdentitySubject: userSubject,
653+
Sponsor: 1,
654+
Role: "viewer",
655+
UpdatedAt: time.Now(),
656+
}, nil)
657+
store.EXPECT().GetUserBySubject(gomock.Any(), userSubject).Return(db.User{
658+
ID: 2,
659+
}, nil)
660+
store.EXPECT().DeleteInvitation(gomock.Any(), gomock.Any()).Return(db.UserInvite{
661+
Project: projectID,
662+
Email: userEmail,
663+
}, nil)
664+
store.EXPECT().GetProjectByID(gomock.Any(), projectID).Return(db.Project{
665+
Name: "project1",
666+
Metadata: projectMetadata,
667+
}, nil)
668+
},
669+
},
670+
{
671+
name: "Success decline",
672+
accept: false,
673+
setup: func(store *mockdb.MockStore) {
674+
store.EXPECT().GetInvitationByCode(gomock.Any(), gomock.Any()).Return(db.GetInvitationByCodeRow{
675+
Project: projectID,
676+
IdentitySubject: userSubject,
677+
Sponsor: 1,
678+
Role: "viewer",
679+
UpdatedAt: time.Now(),
680+
}, nil)
681+
store.EXPECT().GetUserBySubject(gomock.Any(), userSubject).Return(db.User{
682+
ID: 2,
683+
}, nil)
684+
store.EXPECT().DeleteInvitation(gomock.Any(), gomock.Any()).Return(db.UserInvite{
685+
Project: projectID,
686+
Email: userEmail,
687+
}, nil)
688+
store.EXPECT().GetProjectByID(gomock.Any(), projectID).Return(db.Project{
689+
Name: "project1",
690+
Metadata: projectMetadata,
691+
}, nil)
692+
},
693+
},
694+
}
695+
696+
for _, tc := range testCases {
697+
t.Run(tc.name, func(t *testing.T) {
698+
t.Parallel()
699+
700+
ctrl := gomock.NewController(t)
701+
defer ctrl.Finish()
702+
703+
user := openid.New()
704+
assert.NoError(t, user.Set("email", userEmail))
705+
assert.NoError(t, user.Set("sub", userSubject))
706+
707+
ctx := context.Background()
708+
ctx = jwt.WithAuthTokenContext(ctx, user)
709+
710+
mockStore := mockdb.NewMockStore(ctrl)
711+
if tc.setup != nil {
712+
tc.setup(mockStore)
713+
}
714+
715+
featureClient := &flags.FakeClient{}
716+
featureClient.Data = map[string]any{
717+
"user_management": true,
718+
}
719+
720+
authzClient := &mock.SimpleClient{}
721+
722+
server := &Server{
723+
store: mockStore,
724+
featureFlags: featureClient,
725+
authzClient: authzClient,
726+
}
727+
728+
response, err := server.ResolveInvitation(ctx, &pb.ResolveInvitationRequest{
729+
Code: "code",
730+
Accept: tc.accept,
731+
})
732+
733+
if tc.expectedError != "" {
734+
require.Error(t, err)
735+
require.Contains(t, err.Error(), tc.expectedError)
736+
return
737+
}
738+
739+
require.NoError(t, err)
740+
require.Equal(t, tc.accept, response.IsAccepted)
741+
})
742+
}
743+
}

0 commit comments

Comments
 (0)