diff --git a/src/controller/member/controller.go b/src/controller/member/controller.go index 3fddf181b4..813667e399 100644 --- a/src/controller/member/controller.go +++ b/src/controller/member/controller.go @@ -75,14 +75,15 @@ var ErrDuplicateProjectMember = errors.ConflictError(nil).WithMessage("The proje var ErrInvalidRole = errors.BadRequestError(nil).WithMessage("Failed to update project member, role is not in 1,2,3") type controller struct { - userManager user.Manager - mgr member.Manager - projectMgr project.Manager + userManager user.Manager + mgr member.Manager + projectMgr project.Manager + groupManager usergroup.Manager } // NewController ... func NewController() Controller { - return &controller{mgr: member.Mgr, projectMgr: pkg.ProjectMgr, userManager: user.New()} + return &controller{mgr: member.Mgr, projectMgr: pkg.ProjectMgr, userManager: user.New(), groupManager: usergroup.Mgr} } func (c *controller) Count(ctx context.Context, projectNameOrID interface{}, query *q.Query) (int, error) { @@ -129,9 +130,23 @@ func (c *controller) Create(ctx context.Context, projectNameOrID interface{}, re member.EntityType = common.GroupMember if req.MemberUser.UserID > 0 { + user, err := c.userManager.Get(ctx, req.MemberUser.UserID) + if err != nil { + return 0, errors.BadRequestError(nil).WithMessage("Failed to get user %d: %v", req.MemberUser.UserID, err) + } + if user == nil { + return 0, errors.BadRequestError(nil).WithMessage("User %d not found", req.MemberUser.UserID) + } member.EntityID = req.MemberUser.UserID member.EntityType = common.UserMember } else if req.MemberGroup.ID > 0 { + g, err := c.groupManager.Get(ctx, req.MemberGroup.ID) + if err != nil { + return 0, errors.BadRequestError(nil).WithMessage("Failed to get group %d: %v", req.MemberGroup.ID, err) + } + if g == nil { + return 0, errors.BadRequestError(nil).WithMessage("Group %d not found", req.MemberGroup.ID) + } member.EntityID = req.MemberGroup.ID } else if len(req.MemberUser.Username) > 0 { // If username is provided, search userid by username diff --git a/src/controller/member/controller_test.go b/src/controller/member/controller_test.go index 0e06aeea35..c278ef3a51 100644 --- a/src/controller/member/controller_test.go +++ b/src/controller/member/controller_test.go @@ -13,3 +13,88 @@ // limitations under the License. package member + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/suite" + + comModels "github.com/goharbor/harbor/src/common/models" + "github.com/goharbor/harbor/src/pkg/member" + "github.com/goharbor/harbor/src/pkg/project" + "github.com/goharbor/harbor/src/pkg/project/models" + "github.com/goharbor/harbor/src/pkg/user" + "github.com/goharbor/harbor/src/pkg/usergroup" + modelGroup "github.com/goharbor/harbor/src/pkg/usergroup/model" + "github.com/goharbor/harbor/src/testing/mock" + mockMember "github.com/goharbor/harbor/src/testing/pkg/member" + mockProject "github.com/goharbor/harbor/src/testing/pkg/project" + mockUser "github.com/goharbor/harbor/src/testing/pkg/user" + mockUsergroup "github.com/goharbor/harbor/src/testing/pkg/usergroup" +) + +type MemberControllerTestSuite struct { + suite.Suite + userManager user.Manager + memberManager member.Manager + projectMgr project.Manager + groupManager usergroup.Manager + controller *controller +} + +func (suite *MemberControllerTestSuite) SetupSuite() { + suite.userManager = &mockUser.Manager{} + suite.memberManager = &mockMember.Manager{} + suite.projectMgr = &mockProject.Manager{} + suite.groupManager = &mockUsergroup.Manager{} + suite.controller = &controller{ + userManager: suite.userManager, + mgr: suite.memberManager, + projectMgr: suite.projectMgr, + groupManager: suite.groupManager, + } +} + +func (suite *MemberControllerTestSuite) TearDownSuite() { + +} + +func (suite *MemberControllerTestSuite) TestAddProjectMemberWithUser() { + mock.OnAnything(suite.projectMgr, "Get").Return(&models.Project{ + ProjectID: 1, + }, nil) + suite.userManager.(*mockUser.Manager).On("Get", mock.Anything, 1).Return(nil, fmt.Errorf("user not found")) + _, err := suite.controller.Create(nil, 1, Request{MemberUser: User{UserID: 1}}) + suite.Error(err) + _, err = suite.controller.Create(nil, 1, Request{MemberUser: User{UserID: 1}}) + suite.Error(err) + suite.userManager.(*mockUser.Manager).On("Get", mock.Anything, 2).Return(&comModels.User{UserID: 2, Username: "mike"}, nil) + suite.memberManager.(*mockMember.Manager).On("Add", mock.Anything, 1, 2).Return(nil) + mock.OnAnything(suite.memberManager, "List").Return(nil, nil) + mock.OnAnything(suite.memberManager, "AddProjectMember").Return(0, nil) + _, err = suite.controller.Create(nil, 1, Request{MemberUser: User{UserID: 2}, Role: 1}) + suite.NoError(err) +} + +func (suite *MemberControllerTestSuite) TestAddProjectMemberWithUserGroup() { + mock.OnAnything(suite.projectMgr, "Get").Return(&models.Project{ + ProjectID: 1, + }, nil) + suite.groupManager.(*mockUsergroup.Manager).On("Get", mock.Anything, 1).Return(nil, fmt.Errorf("user group not found")) + _, err := suite.controller.Create(nil, 1, Request{MemberGroup: UserGroup{ID: 1}}) + suite.Error(err) + suite.groupManager.(*mockUsergroup.Manager).On("Get", mock.Anything, 1).Return(nil, fmt.Errorf("group not found")) + _, err = suite.controller.Create(nil, 1, Request{MemberGroup: UserGroup{ID: 1}}) + suite.Error(err) + suite.groupManager.(*mockUsergroup.Manager).On("Get", mock.Anything, 2).Return(&modelGroup.UserGroup{ID: 2, GroupName: "group1"}, nil) + suite.memberManager.(*mockMember.Manager).On("Add", mock.Anything, 1, 2).Return(nil) + mock.OnAnything(suite.memberManager, "List").Return(nil, nil) + mock.OnAnything(suite.memberManager, "AddProjectMember").Return(0, nil) + _, err = suite.controller.Create(nil, 1, Request{MemberGroup: UserGroup{ID: 2}, Role: 1}) + suite.NoError(err) +} + +func TestMemberControllerTestSuite(t *testing.T) { + suite.Run(t, &MemberControllerTestSuite{}) +} diff --git a/src/testing/pkg/member/fake_member_manager.go b/src/testing/pkg/member/fake_member_manager.go new file mode 100644 index 0000000000..743c2052f8 --- /dev/null +++ b/src/testing/pkg/member/fake_member_manager.go @@ -0,0 +1,216 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package member + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + models "github.com/goharbor/harbor/src/pkg/member/models" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// AddProjectMember provides a mock function with given fields: ctx, _a1 +func (_m *Manager) AddProjectMember(ctx context.Context, _a1 models.Member) (int, error) { + ret := _m.Called(ctx, _a1) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, models.Member) int); ok { + r0 = rf(ctx, _a1) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.Member) error); ok { + r1 = rf(ctx, _a1) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, projectID, memberID +func (_m *Manager) Delete(ctx context.Context, projectID int64, memberID int) error { + ret := _m.Called(ctx, projectID, memberID) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int) error); ok { + r0 = rf(ctx, projectID, memberID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// DeleteMemberByUserID provides a mock function with given fields: ctx, uid +func (_m *Manager) DeleteMemberByUserID(ctx context.Context, uid int) error { + ret := _m.Called(ctx, uid) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, uid) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, projectID, memberID +func (_m *Manager) Get(ctx context.Context, projectID int64, memberID int) (*models.Member, error) { + ret := _m.Called(ctx, projectID, memberID) + + var r0 *models.Member + if rf, ok := ret.Get(0).(func(context.Context, int64, int) *models.Member); ok { + r0 = rf(ctx, projectID, memberID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*models.Member) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, int) error); ok { + r1 = rf(ctx, projectID, memberID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// GetTotalOfProjectMembers provides a mock function with given fields: ctx, projectID, query, roles +func (_m *Manager) GetTotalOfProjectMembers(ctx context.Context, projectID int64, query *q.Query, roles ...int) (int, error) { + _va := make([]interface{}, len(roles)) + for _i := range roles { + _va[_i] = roles[_i] + } + var _ca []interface{} + _ca = append(_ca, ctx, projectID, query) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, int64, *q.Query, ...int) int); ok { + r0 = rf(ctx, projectID, query, roles...) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, *q.Query, ...int) error); ok { + r1 = rf(ctx, projectID, query, roles...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, queryMember, query +func (_m *Manager) List(ctx context.Context, queryMember models.Member, query *q.Query) ([]*models.Member, error) { + ret := _m.Called(ctx, queryMember, query) + + var r0 []*models.Member + if rf, ok := ret.Get(0).(func(context.Context, models.Member, *q.Query) []*models.Member); ok { + r0 = rf(ctx, queryMember, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Member) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, models.Member, *q.Query) error); ok { + r1 = rf(ctx, queryMember, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// ListRoles provides a mock function with given fields: ctx, user, projectID +func (_m *Manager) ListRoles(ctx context.Context, user *models.User, projectID int64) ([]int, error) { + ret := _m.Called(ctx, user, projectID) + + var r0 []int + if rf, ok := ret.Get(0).(func(context.Context, *models.User, int64) []int); ok { + r0 = rf(ctx, user, projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *models.User, int64) error); ok { + r1 = rf(ctx, user, projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// SearchMemberByName provides a mock function with given fields: ctx, projectID, entityName +func (_m *Manager) SearchMemberByName(ctx context.Context, projectID int64, entityName string) ([]*models.Member, error) { + ret := _m.Called(ctx, projectID, entityName) + + var r0 []*models.Member + if rf, ok := ret.Get(0).(func(context.Context, int64, string) []*models.Member); ok { + r0 = rf(ctx, projectID, entityName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*models.Member) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int64, string) error); ok { + r1 = rf(ctx, projectID, entityName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateRole provides a mock function with given fields: ctx, projectID, pmID, role +func (_m *Manager) UpdateRole(ctx context.Context, projectID int64, pmID int, role int) error { + ret := _m.Called(ctx, projectID, pmID, role) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int64, int, int) error); ok { + r0 = rf(ctx, projectID, pmID, role) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewManager interface { + mock.TestingT + Cleanup(func()) +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewManager(t mockConstructorTestingTNewManager) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/src/testing/pkg/pkg.go b/src/testing/pkg/pkg.go index ca2acbd5e1..448847d6a6 100644 --- a/src/testing/pkg/pkg.go +++ b/src/testing/pkg/pkg.go @@ -66,3 +66,5 @@ package pkg //go:generate mockery --case snake --dir ../../pkg/scan/export --name Manager --output ./scan/export --outpkg export //go:generate mockery --case snake --dir ../../pkg/scan/export --name ArtifactDigestCalculator --output ./scan/export --outpkg export //go:generate mockery --case snake --dir ../../pkg/registry --name Client --output ./registry --outpkg registry --filename fake_registry_client.go +//go:generate mockery --case snake --dir ../../pkg/member --name Manager --output ./member --outpkg member --filename fake_member_manager.go +//go:generate mockery --case snake --dir ../../pkg/usergroup --name Manager --output ./usergroup --outpkg usergroup --filename fake_usergroup_manager.go diff --git a/src/testing/pkg/usergroup/fake_usergroup_manager.go b/src/testing/pkg/usergroup/fake_usergroup_manager.go new file mode 100644 index 0000000000..3127d31df9 --- /dev/null +++ b/src/testing/pkg/usergroup/fake_usergroup_manager.go @@ -0,0 +1,185 @@ +// Code generated by mockery v2.14.0. DO NOT EDIT. + +package usergroup + +import ( + context "context" + + model "github.com/goharbor/harbor/src/pkg/usergroup/model" + mock "github.com/stretchr/testify/mock" + + q "github.com/goharbor/harbor/src/lib/q" +) + +// Manager is an autogenerated mock type for the Manager type +type Manager struct { + mock.Mock +} + +// Count provides a mock function with given fields: ctx, query +func (_m *Manager) Count(ctx context.Context, query *q.Query) (int64, error) { + ret := _m.Called(ctx, query) + + var r0 int64 + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) int64); ok { + r0 = rf(ctx, query) + } else { + r0 = ret.Get(0).(int64) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Create provides a mock function with given fields: ctx, userGroup +func (_m *Manager) Create(ctx context.Context, userGroup model.UserGroup) (int, error) { + ret := _m.Called(ctx, userGroup) + + var r0 int + if rf, ok := ret.Get(0).(func(context.Context, model.UserGroup) int); ok { + r0 = rf(ctx, userGroup) + } else { + r0 = ret.Get(0).(int) + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, model.UserGroup) error); ok { + r1 = rf(ctx, userGroup) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Delete provides a mock function with given fields: ctx, id +func (_m *Manager) Delete(ctx context.Context, id int) error { + ret := _m.Called(ctx, id) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int) error); ok { + r0 = rf(ctx, id) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Get provides a mock function with given fields: ctx, id +func (_m *Manager) Get(ctx context.Context, id int) (*model.UserGroup, error) { + ret := _m.Called(ctx, id) + + var r0 *model.UserGroup + if rf, ok := ret.Get(0).(func(context.Context, int) *model.UserGroup); ok { + r0 = rf(ctx, id) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*model.UserGroup) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, int) error); ok { + r1 = rf(ctx, id) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// List provides a mock function with given fields: ctx, query +func (_m *Manager) List(ctx context.Context, query *q.Query) ([]*model.UserGroup, error) { + ret := _m.Called(ctx, query) + + var r0 []*model.UserGroup + if rf, ok := ret.Get(0).(func(context.Context, *q.Query) []*model.UserGroup); ok { + r0 = rf(ctx, query) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]*model.UserGroup) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *q.Query) error); ok { + r1 = rf(ctx, query) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// Onboard provides a mock function with given fields: ctx, g +func (_m *Manager) Onboard(ctx context.Context, g *model.UserGroup) error { + ret := _m.Called(ctx, g) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *model.UserGroup) error); ok { + r0 = rf(ctx, g) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// Populate provides a mock function with given fields: ctx, userGroups +func (_m *Manager) Populate(ctx context.Context, userGroups []model.UserGroup) ([]int, error) { + ret := _m.Called(ctx, userGroups) + + var r0 []int + if rf, ok := ret.Get(0).(func(context.Context, []model.UserGroup) []int); ok { + r0 = rf(ctx, userGroups) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, []model.UserGroup) error); ok { + r1 = rf(ctx, userGroups) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// UpdateName provides a mock function with given fields: ctx, id, groupName +func (_m *Manager) UpdateName(ctx context.Context, id int, groupName string) error { + ret := _m.Called(ctx, id, groupName) + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, int, string) error); ok { + r0 = rf(ctx, id, groupName) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +type mockConstructorTestingTNewManager interface { + mock.TestingT + Cleanup(func()) +} + +// NewManager creates a new instance of Manager. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +func NewManager(t mockConstructorTestingTNewManager) *Manager { + mock := &Manager{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +}