From a545b353d018b03872ec522eb3affff56d099afb Mon Sep 17 00:00:00 2001 From: cveticm <119604954+cveticm@users.noreply.github.com> Date: Thu, 10 Oct 2024 12:57:59 +0100 Subject: [PATCH] CLOUDP-276151: Migrate Assigned Teams Translation Layer (#1850) * Translation layer implementation with testing * Lint changes * Translation Unit test fix * Fix to conversion * Fix to conversion * Implementing review feedback * Altered type structs Team and AssignedTeam * Lint fix * Addition of subservices for TeamsService * Fixed manifests and removed unused teams sub service mocks * Additional teams/conversion.go unit test coverage --- .mockery.yaml | 1 + internal/mocks/translation/teams_service.go | 642 ++++++++++++++++++ internal/translation/teams/conversion.go | 135 ++++ internal/translation/teams/conversion_test.go | 83 +++ internal/translation/teams/teams.go | 141 ++++ internal/translation/teams/teams_test.go | 634 +++++++++++++++++ .../atlasproject/atlasproject_controller.go | 4 +- pkg/controller/atlasproject/project_test.go | 100 ++- .../atlasproject/team_reconciler.go | 122 ++-- .../atlasproject/team_reconciler_test.go | 154 ++--- pkg/controller/atlasproject/teams.go | 78 ++- pkg/controller/atlasproject/teams_test.go | 80 +-- 12 files changed, 1892 insertions(+), 282 deletions(-) create mode 100644 internal/mocks/translation/teams_service.go create mode 100644 internal/translation/teams/conversion.go create mode 100644 internal/translation/teams/conversion_test.go create mode 100644 internal/translation/teams/teams.go create mode 100644 internal/translation/teams/teams_test.go diff --git a/.mockery.yaml b/.mockery.yaml index 4357e4e91d..3b84385c74 100644 --- a/.mockery.yaml +++ b/.mockery.yaml @@ -11,3 +11,4 @@ packages: github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/dbuser: github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/deployment: github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/customroles: + github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams: diff --git a/internal/mocks/translation/teams_service.go b/internal/mocks/translation/teams_service.go new file mode 100644 index 0000000000..83c3a33896 --- /dev/null +++ b/internal/mocks/translation/teams_service.go @@ -0,0 +1,642 @@ +// Code generated by mockery. DO NOT EDIT. + +package translation + +import ( + context "context" + + mock "github.com/stretchr/testify/mock" + + teams "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" + v1 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +// TeamsServiceMock is an autogenerated mock type for the TeamsService type +type TeamsServiceMock struct { + mock.Mock +} + +type TeamsServiceMock_Expecter struct { + mock *mock.Mock +} + +func (_m *TeamsServiceMock) EXPECT() *TeamsServiceMock_Expecter { + return &TeamsServiceMock_Expecter{mock: &_m.Mock} +} + +// AddUsers provides a mock function with given fields: ctx, usersToAdd, orgID, teamID +func (_m *TeamsServiceMock) AddUsers(ctx context.Context, usersToAdd *[]teams.TeamUser, orgID string, teamID string) error { + ret := _m.Called(ctx, usersToAdd, orgID, teamID) + + if len(ret) == 0 { + panic("no return value specified for AddUsers") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *[]teams.TeamUser, string, string) error); ok { + r0 = rf(ctx, usersToAdd, orgID, teamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TeamsServiceMock_AddUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'AddUsers' +type TeamsServiceMock_AddUsers_Call struct { + *mock.Call +} + +// AddUsers is a helper method to define mock.On call +// - ctx context.Context +// - usersToAdd *[]teams.TeamUser +// - orgID string +// - teamID string +func (_e *TeamsServiceMock_Expecter) AddUsers(ctx interface{}, usersToAdd interface{}, orgID interface{}, teamID interface{}) *TeamsServiceMock_AddUsers_Call { + return &TeamsServiceMock_AddUsers_Call{Call: _e.mock.On("AddUsers", ctx, usersToAdd, orgID, teamID)} +} + +func (_c *TeamsServiceMock_AddUsers_Call) Run(run func(ctx context.Context, usersToAdd *[]teams.TeamUser, orgID string, teamID string)) *TeamsServiceMock_AddUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*[]teams.TeamUser), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_AddUsers_Call) Return(_a0 error) *TeamsServiceMock_AddUsers_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TeamsServiceMock_AddUsers_Call) RunAndReturn(run func(context.Context, *[]teams.TeamUser, string, string) error) *TeamsServiceMock_AddUsers_Call { + _c.Call.Return(run) + return _c +} + +// Assign provides a mock function with given fields: ctx, at, projectID +func (_m *TeamsServiceMock) Assign(ctx context.Context, at *[]teams.AssignedTeam, projectID string) error { + ret := _m.Called(ctx, at, projectID) + + if len(ret) == 0 { + panic("no return value specified for Assign") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *[]teams.AssignedTeam, string) error); ok { + r0 = rf(ctx, at, projectID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TeamsServiceMock_Assign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Assign' +type TeamsServiceMock_Assign_Call struct { + *mock.Call +} + +// Assign is a helper method to define mock.On call +// - ctx context.Context +// - at *[]teams.AssignedTeam +// - projectID string +func (_e *TeamsServiceMock_Expecter) Assign(ctx interface{}, at interface{}, projectID interface{}) *TeamsServiceMock_Assign_Call { + return &TeamsServiceMock_Assign_Call{Call: _e.mock.On("Assign", ctx, at, projectID)} +} + +func (_c *TeamsServiceMock_Assign_Call) Run(run func(ctx context.Context, at *[]teams.AssignedTeam, projectID string)) *TeamsServiceMock_Assign_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*[]teams.AssignedTeam), args[2].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_Assign_Call) Return(_a0 error) *TeamsServiceMock_Assign_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TeamsServiceMock_Assign_Call) RunAndReturn(run func(context.Context, *[]teams.AssignedTeam, string) error) *TeamsServiceMock_Assign_Call { + _c.Call.Return(run) + return _c +} + +// Create provides a mock function with given fields: ctx, at, orgID +func (_m *TeamsServiceMock) Create(ctx context.Context, at *teams.Team, orgID string) (*teams.Team, error) { + ret := _m.Called(ctx, at, orgID) + + if len(ret) == 0 { + panic("no return value specified for Create") + } + + var r0 *teams.Team + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *teams.Team, string) (*teams.Team, error)); ok { + return rf(ctx, at, orgID) + } + if rf, ok := ret.Get(0).(func(context.Context, *teams.Team, string) *teams.Team); ok { + r0 = rf(ctx, at, orgID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*teams.Team) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *teams.Team, string) error); ok { + r1 = rf(ctx, at, orgID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TeamsServiceMock_Create_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Create' +type TeamsServiceMock_Create_Call struct { + *mock.Call +} + +// Create is a helper method to define mock.On call +// - ctx context.Context +// - at *teams.Team +// - orgID string +func (_e *TeamsServiceMock_Expecter) Create(ctx interface{}, at interface{}, orgID interface{}) *TeamsServiceMock_Create_Call { + return &TeamsServiceMock_Create_Call{Call: _e.mock.On("Create", ctx, at, orgID)} +} + +func (_c *TeamsServiceMock_Create_Call) Run(run func(ctx context.Context, at *teams.Team, orgID string)) *TeamsServiceMock_Create_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*teams.Team), args[2].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_Create_Call) Return(_a0 *teams.Team, _a1 error) *TeamsServiceMock_Create_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TeamsServiceMock_Create_Call) RunAndReturn(run func(context.Context, *teams.Team, string) (*teams.Team, error)) *TeamsServiceMock_Create_Call { + _c.Call.Return(run) + return _c +} + +// GetTeamByID provides a mock function with given fields: ctx, orgID, teamID +func (_m *TeamsServiceMock) GetTeamByID(ctx context.Context, orgID string, teamID string) (*teams.AssignedTeam, error) { + ret := _m.Called(ctx, orgID, teamID) + + if len(ret) == 0 { + panic("no return value specified for GetTeamByID") + } + + var r0 *teams.AssignedTeam + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*teams.AssignedTeam, error)); ok { + return rf(ctx, orgID, teamID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *teams.AssignedTeam); ok { + r0 = rf(ctx, orgID, teamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*teams.AssignedTeam) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, orgID, teamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TeamsServiceMock_GetTeamByID_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTeamByID' +type TeamsServiceMock_GetTeamByID_Call struct { + *mock.Call +} + +// GetTeamByID is a helper method to define mock.On call +// - ctx context.Context +// - orgID string +// - teamID string +func (_e *TeamsServiceMock_Expecter) GetTeamByID(ctx interface{}, orgID interface{}, teamID interface{}) *TeamsServiceMock_GetTeamByID_Call { + return &TeamsServiceMock_GetTeamByID_Call{Call: _e.mock.On("GetTeamByID", ctx, orgID, teamID)} +} + +func (_c *TeamsServiceMock_GetTeamByID_Call) Run(run func(ctx context.Context, orgID string, teamID string)) *TeamsServiceMock_GetTeamByID_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_GetTeamByID_Call) Return(_a0 *teams.AssignedTeam, _a1 error) *TeamsServiceMock_GetTeamByID_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TeamsServiceMock_GetTeamByID_Call) RunAndReturn(run func(context.Context, string, string) (*teams.AssignedTeam, error)) *TeamsServiceMock_GetTeamByID_Call { + _c.Call.Return(run) + return _c +} + +// GetTeamByName provides a mock function with given fields: ctx, orgID, teamName +func (_m *TeamsServiceMock) GetTeamByName(ctx context.Context, orgID string, teamName string) (*teams.AssignedTeam, error) { + ret := _m.Called(ctx, orgID, teamName) + + if len(ret) == 0 { + panic("no return value specified for GetTeamByName") + } + + var r0 *teams.AssignedTeam + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) (*teams.AssignedTeam, error)); ok { + return rf(ctx, orgID, teamName) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) *teams.AssignedTeam); ok { + r0 = rf(ctx, orgID, teamName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*teams.AssignedTeam) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, orgID, teamName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TeamsServiceMock_GetTeamByName_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTeamByName' +type TeamsServiceMock_GetTeamByName_Call struct { + *mock.Call +} + +// GetTeamByName is a helper method to define mock.On call +// - ctx context.Context +// - orgID string +// - teamName string +func (_e *TeamsServiceMock_Expecter) GetTeamByName(ctx interface{}, orgID interface{}, teamName interface{}) *TeamsServiceMock_GetTeamByName_Call { + return &TeamsServiceMock_GetTeamByName_Call{Call: _e.mock.On("GetTeamByName", ctx, orgID, teamName)} +} + +func (_c *TeamsServiceMock_GetTeamByName_Call) Run(run func(ctx context.Context, orgID string, teamName string)) *TeamsServiceMock_GetTeamByName_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_GetTeamByName_Call) Return(_a0 *teams.AssignedTeam, _a1 error) *TeamsServiceMock_GetTeamByName_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TeamsServiceMock_GetTeamByName_Call) RunAndReturn(run func(context.Context, string, string) (*teams.AssignedTeam, error)) *TeamsServiceMock_GetTeamByName_Call { + _c.Call.Return(run) + return _c +} + +// GetTeamUsers provides a mock function with given fields: ctx, orgID, teamID +func (_m *TeamsServiceMock) GetTeamUsers(ctx context.Context, orgID string, teamID string) ([]teams.TeamUser, error) { + ret := _m.Called(ctx, orgID, teamID) + + if len(ret) == 0 { + panic("no return value specified for GetTeamUsers") + } + + var r0 []teams.TeamUser + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) ([]teams.TeamUser, error)); ok { + return rf(ctx, orgID, teamID) + } + if rf, ok := ret.Get(0).(func(context.Context, string, string) []teams.TeamUser); ok { + r0 = rf(ctx, orgID, teamID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]teams.TeamUser) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string, string) error); ok { + r1 = rf(ctx, orgID, teamID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TeamsServiceMock_GetTeamUsers_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'GetTeamUsers' +type TeamsServiceMock_GetTeamUsers_Call struct { + *mock.Call +} + +// GetTeamUsers is a helper method to define mock.On call +// - ctx context.Context +// - orgID string +// - teamID string +func (_e *TeamsServiceMock_Expecter) GetTeamUsers(ctx interface{}, orgID interface{}, teamID interface{}) *TeamsServiceMock_GetTeamUsers_Call { + return &TeamsServiceMock_GetTeamUsers_Call{Call: _e.mock.On("GetTeamUsers", ctx, orgID, teamID)} +} + +func (_c *TeamsServiceMock_GetTeamUsers_Call) Run(run func(ctx context.Context, orgID string, teamID string)) *TeamsServiceMock_GetTeamUsers_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_GetTeamUsers_Call) Return(_a0 []teams.TeamUser, _a1 error) *TeamsServiceMock_GetTeamUsers_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TeamsServiceMock_GetTeamUsers_Call) RunAndReturn(run func(context.Context, string, string) ([]teams.TeamUser, error)) *TeamsServiceMock_GetTeamUsers_Call { + _c.Call.Return(run) + return _c +} + +// ListProjectTeams provides a mock function with given fields: ctx, projectID +func (_m *TeamsServiceMock) ListProjectTeams(ctx context.Context, projectID string) ([]teams.AssignedTeam, error) { + ret := _m.Called(ctx, projectID) + + if len(ret) == 0 { + panic("no return value specified for ListProjectTeams") + } + + var r0 []teams.AssignedTeam + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, string) ([]teams.AssignedTeam, error)); ok { + return rf(ctx, projectID) + } + if rf, ok := ret.Get(0).(func(context.Context, string) []teams.AssignedTeam); ok { + r0 = rf(ctx, projectID) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).([]teams.AssignedTeam) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, string) error); ok { + r1 = rf(ctx, projectID) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TeamsServiceMock_ListProjectTeams_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'ListProjectTeams' +type TeamsServiceMock_ListProjectTeams_Call struct { + *mock.Call +} + +// ListProjectTeams is a helper method to define mock.On call +// - ctx context.Context +// - projectID string +func (_e *TeamsServiceMock_Expecter) ListProjectTeams(ctx interface{}, projectID interface{}) *TeamsServiceMock_ListProjectTeams_Call { + return &TeamsServiceMock_ListProjectTeams_Call{Call: _e.mock.On("ListProjectTeams", ctx, projectID)} +} + +func (_c *TeamsServiceMock_ListProjectTeams_Call) Run(run func(ctx context.Context, projectID string)) *TeamsServiceMock_ListProjectTeams_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_ListProjectTeams_Call) Return(_a0 []teams.AssignedTeam, _a1 error) *TeamsServiceMock_ListProjectTeams_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TeamsServiceMock_ListProjectTeams_Call) RunAndReturn(run func(context.Context, string) ([]teams.AssignedTeam, error)) *TeamsServiceMock_ListProjectTeams_Call { + _c.Call.Return(run) + return _c +} + +// RemoveUser provides a mock function with given fields: ctx, orgID, teamID, userID +func (_m *TeamsServiceMock) RemoveUser(ctx context.Context, orgID string, teamID string, userID string) error { + ret := _m.Called(ctx, orgID, teamID, userID) + + if len(ret) == 0 { + panic("no return value specified for RemoveUser") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string, string) error); ok { + r0 = rf(ctx, orgID, teamID, userID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TeamsServiceMock_RemoveUser_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RemoveUser' +type TeamsServiceMock_RemoveUser_Call struct { + *mock.Call +} + +// RemoveUser is a helper method to define mock.On call +// - ctx context.Context +// - orgID string +// - teamID string +// - userID string +func (_e *TeamsServiceMock_Expecter) RemoveUser(ctx interface{}, orgID interface{}, teamID interface{}, userID interface{}) *TeamsServiceMock_RemoveUser_Call { + return &TeamsServiceMock_RemoveUser_Call{Call: _e.mock.On("RemoveUser", ctx, orgID, teamID, userID)} +} + +func (_c *TeamsServiceMock_RemoveUser_Call) Run(run func(ctx context.Context, orgID string, teamID string, userID string)) *TeamsServiceMock_RemoveUser_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_RemoveUser_Call) Return(_a0 error) *TeamsServiceMock_RemoveUser_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TeamsServiceMock_RemoveUser_Call) RunAndReturn(run func(context.Context, string, string, string) error) *TeamsServiceMock_RemoveUser_Call { + _c.Call.Return(run) + return _c +} + +// RenameTeam provides a mock function with given fields: ctx, at, orgID, newName +func (_m *TeamsServiceMock) RenameTeam(ctx context.Context, at *teams.AssignedTeam, orgID string, newName string) (*teams.AssignedTeam, error) { + ret := _m.Called(ctx, at, orgID, newName) + + if len(ret) == 0 { + panic("no return value specified for RenameTeam") + } + + var r0 *teams.AssignedTeam + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, *teams.AssignedTeam, string, string) (*teams.AssignedTeam, error)); ok { + return rf(ctx, at, orgID, newName) + } + if rf, ok := ret.Get(0).(func(context.Context, *teams.AssignedTeam, string, string) *teams.AssignedTeam); ok { + r0 = rf(ctx, at, orgID, newName) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*teams.AssignedTeam) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, *teams.AssignedTeam, string, string) error); ok { + r1 = rf(ctx, at, orgID, newName) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// TeamsServiceMock_RenameTeam_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'RenameTeam' +type TeamsServiceMock_RenameTeam_Call struct { + *mock.Call +} + +// RenameTeam is a helper method to define mock.On call +// - ctx context.Context +// - at *teams.AssignedTeam +// - orgID string +// - newName string +func (_e *TeamsServiceMock_Expecter) RenameTeam(ctx interface{}, at interface{}, orgID interface{}, newName interface{}) *TeamsServiceMock_RenameTeam_Call { + return &TeamsServiceMock_RenameTeam_Call{Call: _e.mock.On("RenameTeam", ctx, at, orgID, newName)} +} + +func (_c *TeamsServiceMock_RenameTeam_Call) Run(run func(ctx context.Context, at *teams.AssignedTeam, orgID string, newName string)) *TeamsServiceMock_RenameTeam_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*teams.AssignedTeam), args[2].(string), args[3].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_RenameTeam_Call) Return(_a0 *teams.AssignedTeam, _a1 error) *TeamsServiceMock_RenameTeam_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *TeamsServiceMock_RenameTeam_Call) RunAndReturn(run func(context.Context, *teams.AssignedTeam, string, string) (*teams.AssignedTeam, error)) *TeamsServiceMock_RenameTeam_Call { + _c.Call.Return(run) + return _c +} + +// Unassign provides a mock function with given fields: ctx, projectID, teamID +func (_m *TeamsServiceMock) Unassign(ctx context.Context, projectID string, teamID string) error { + ret := _m.Called(ctx, projectID, teamID) + + if len(ret) == 0 { + panic("no return value specified for Unassign") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, string, string) error); ok { + r0 = rf(ctx, projectID, teamID) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TeamsServiceMock_Unassign_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'Unassign' +type TeamsServiceMock_Unassign_Call struct { + *mock.Call +} + +// Unassign is a helper method to define mock.On call +// - ctx context.Context +// - projectID string +// - teamID string +func (_e *TeamsServiceMock_Expecter) Unassign(ctx interface{}, projectID interface{}, teamID interface{}) *TeamsServiceMock_Unassign_Call { + return &TeamsServiceMock_Unassign_Call{Call: _e.mock.On("Unassign", ctx, projectID, teamID)} +} + +func (_c *TeamsServiceMock_Unassign_Call) Run(run func(ctx context.Context, projectID string, teamID string)) *TeamsServiceMock_Unassign_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(string), args[2].(string)) + }) + return _c +} + +func (_c *TeamsServiceMock_Unassign_Call) Return(_a0 error) *TeamsServiceMock_Unassign_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TeamsServiceMock_Unassign_Call) RunAndReturn(run func(context.Context, string, string) error) *TeamsServiceMock_Unassign_Call { + _c.Call.Return(run) + return _c +} + +// UpdateRoles provides a mock function with given fields: ctx, at, projectID, newRoles +func (_m *TeamsServiceMock) UpdateRoles(ctx context.Context, at *teams.AssignedTeam, projectID string, newRoles []v1.TeamRole) error { + ret := _m.Called(ctx, at, projectID, newRoles) + + if len(ret) == 0 { + panic("no return value specified for UpdateRoles") + } + + var r0 error + if rf, ok := ret.Get(0).(func(context.Context, *teams.AssignedTeam, string, []v1.TeamRole) error); ok { + r0 = rf(ctx, at, projectID, newRoles) + } else { + r0 = ret.Error(0) + } + + return r0 +} + +// TeamsServiceMock_UpdateRoles_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'UpdateRoles' +type TeamsServiceMock_UpdateRoles_Call struct { + *mock.Call +} + +// UpdateRoles is a helper method to define mock.On call +// - ctx context.Context +// - at *teams.AssignedTeam +// - projectID string +// - newRoles []v1.TeamRole +func (_e *TeamsServiceMock_Expecter) UpdateRoles(ctx interface{}, at interface{}, projectID interface{}, newRoles interface{}) *TeamsServiceMock_UpdateRoles_Call { + return &TeamsServiceMock_UpdateRoles_Call{Call: _e.mock.On("UpdateRoles", ctx, at, projectID, newRoles)} +} + +func (_c *TeamsServiceMock_UpdateRoles_Call) Run(run func(ctx context.Context, at *teams.AssignedTeam, projectID string, newRoles []v1.TeamRole)) *TeamsServiceMock_UpdateRoles_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].(*teams.AssignedTeam), args[2].(string), args[3].([]v1.TeamRole)) + }) + return _c +} + +func (_c *TeamsServiceMock_UpdateRoles_Call) Return(_a0 error) *TeamsServiceMock_UpdateRoles_Call { + _c.Call.Return(_a0) + return _c +} + +func (_c *TeamsServiceMock_UpdateRoles_Call) RunAndReturn(run func(context.Context, *teams.AssignedTeam, string, []v1.TeamRole) error) *TeamsServiceMock_UpdateRoles_Call { + _c.Call.Return(run) + return _c +} + +// NewTeamsServiceMock creates a new instance of TeamsServiceMock. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewTeamsServiceMock(t interface { + mock.TestingT + Cleanup(func()) +}) *TeamsServiceMock { + mock := &TeamsServiceMock{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/internal/translation/teams/conversion.go b/internal/translation/teams/conversion.go new file mode 100644 index 0000000000..8aa937dd65 --- /dev/null +++ b/internal/translation/teams/conversion.go @@ -0,0 +1,135 @@ +package teams + +import ( + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +type Team struct { + Usernames []string + TeamID string + TeamName string +} + +type AssignedTeam struct { + Roles []string + TeamID string + TeamName string +} + +type TeamUser struct { + Username string + UserID string +} + +func NewTeam(teamSpec *akov2.TeamSpec, teamID string) *Team { + if teamSpec == nil { + return nil + } + usernames := make([]string, 0, len(teamSpec.Usernames)) + for _, username := range teamSpec.Usernames { + usernames = append(usernames, string(username)) + } + + team := &Team{ + TeamID: teamID, + TeamName: teamSpec.Name, + Usernames: usernames, + } + + return team +} + +func NewAssignedTeam(projTeamSpec *akov2.Team, teamID string) *AssignedTeam { + if projTeamSpec == nil { + return nil + } + + roles := make([]string, 0, len(projTeamSpec.Roles)) + for _, role := range projTeamSpec.Roles { + roles = append(roles, string(role)) + } + + team := &AssignedTeam{ + Roles: roles, + TeamID: teamID, + } + + return team +} + +func TeamFromAtlas(assignedTeam *admin.TeamResponse) *Team { + return &Team{ + TeamID: assignedTeam.GetId(), + TeamName: assignedTeam.GetName(), + } +} + +func TeamToAtlas(team *Team) *admin.Team { + return &admin.Team{ + Id: pointer.MakePtrOrNil(team.TeamID), + Name: team.TeamName, + Usernames: &team.Usernames, + } +} + +func AssignedTeamFromAtlas(team *admin.TeamResponse) *AssignedTeam { + if team == nil { + return nil + } + + tm := &AssignedTeam{ + TeamID: team.GetId(), + TeamName: team.GetName(), + } + return tm +} + +func TeamRolesFromAtlas(atlasTeams []admin.TeamRole) []AssignedTeam { + teams := make([]AssignedTeam, 0, len(atlasTeams)) + for _, team := range atlasTeams { + teams = append(teams, AssignedTeam{Roles: team.GetRoleNames(), TeamID: team.GetTeamId()}) + } + return teams +} + +func TeamRolesToAtlas(atlasTeams []AssignedTeam) []admin.TeamRole { + if atlasTeams == nil { + return nil + } + teams := make([]admin.TeamRole, 0, len(atlasTeams)) + + for _, team := range atlasTeams { + result := admin.TeamRole{ + TeamId: pointer.MakePtrOrNil(team.TeamID), + RoleNames: &team.Roles, + } + teams = append(teams, result) + } + return teams +} + +func UsersFromAtlas(users *admin.PaginatedApiAppUser) []TeamUser { + teamUsers := make([]TeamUser, 0, len(users.GetResults())) + for _, user := range users.GetResults() { + teamUsers = append(teamUsers, TeamUser{ + Username: user.Username, + UserID: user.GetId(), + }) + } + return teamUsers +} + +func UsersToAtlas(teamUsers *[]TeamUser) *[]admin.AddUserToTeam { + users := *teamUsers + desiredUsers := make([]admin.AddUserToTeam, 0, len(users)) + for _, user := range users { + desiredUsers = append(desiredUsers, admin.AddUserToTeam{ + Id: user.UserID, + }) + } + return &desiredUsers +} diff --git a/internal/translation/teams/conversion_test.go b/internal/translation/teams/conversion_test.go new file mode 100644 index 0000000000..c0a765aa85 --- /dev/null +++ b/internal/translation/teams/conversion_test.go @@ -0,0 +1,83 @@ +package teams + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +const ( + testTeamID = "team-id" +) + +func TestNewTeam(t *testing.T) { + for _, tc := range []struct { + title string + teamSpec *akov2.TeamSpec + teamID string + expectedTeam *Team + }{ + { + title: "Nil spec returns nil user", + }, + { + title: "Empty spec returns Empty user", + teamSpec: &akov2.TeamSpec{}, + expectedTeam: &Team{Usernames: []string{}}, + }, + { + title: "Populated spec is properly created", + teamSpec: &akov2.TeamSpec{ + Name: testTeamName, + Usernames: []akov2.TeamUser{"user1", "user2"}, + }, + teamID: testTeamID, + expectedTeam: &Team{ + TeamName: testTeamName, + TeamID: testTeamID, + Usernames: []string{"user1", "user2"}, + }, + }, + } { + t.Run(tc.title, func(t *testing.T) { + team := NewTeam(tc.teamSpec, tc.teamID) + assert.Equal(t, tc.expectedTeam, team) + }) + } +} + +func TestNewAssignedTeam(t *testing.T) { + for _, tc := range []struct { + title string + projTeamSpec *akov2.Team + teamID string + expectedTeam *AssignedTeam + }{ + { + title: "Nil spec returns nil user", + }, + { + title: "Empty spec returns Empty user", + projTeamSpec: &akov2.Team{}, + expectedTeam: &AssignedTeam{Roles: []string{}}, + }, + { + title: "Populated spec is properly created", + projTeamSpec: &akov2.Team{ + Roles: []akov2.TeamRole{"role1", "role2"}, + }, + teamID: testTeamID, + expectedTeam: &AssignedTeam{ + Roles: []string{"role1", "role2"}, + TeamID: testTeamID, + }, + }, + } { + t.Run(tc.title, func(t *testing.T) { + team := NewAssignedTeam(tc.projTeamSpec, tc.teamID) + assert.Equal(t, tc.expectedTeam, team) + }) + } +} diff --git a/internal/translation/teams/teams.go b/internal/translation/teams/teams.go new file mode 100644 index 0000000000..03a65b827d --- /dev/null +++ b/internal/translation/teams/teams.go @@ -0,0 +1,141 @@ +package teams + +import ( + "context" + "fmt" + "net/http" + + "go.mongodb.org/atlas-sdk/v20231115008/admin" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +type TeamsService interface { + TeamProjectsService + TeamRolesService + TeamUsersService +} + +type TeamProjectsService interface { // manages Team's associations to Projects + ListProjectTeams(ctx context.Context, projectID string) ([]AssignedTeam, error) + Create(ctx context.Context, at *Team, orgID string) (*Team, error) + Assign(ctx context.Context, at *[]AssignedTeam, projectID string) error + Unassign(ctx context.Context, projectID, teamID string) error +} + +type TeamRolesService interface { // manages Team's Roles + GetTeamByName(ctx context.Context, orgID, teamName string) (*AssignedTeam, error) + GetTeamByID(ctx context.Context, orgID, teamID string) (*AssignedTeam, error) + RenameTeam(ctx context.Context, at *AssignedTeam, orgID, newName string) (*AssignedTeam, error) + UpdateRoles(ctx context.Context, at *AssignedTeam, projectID string, newRoles []akov2.TeamRole) error +} + +type TeamUsersService interface { // manages Team's Members (Users) + GetTeamUsers(ctx context.Context, orgID, teamID string) ([]TeamUser, error) + AddUsers(ctx context.Context, usersToAdd *[]TeamUser, orgID, teamID string) error + RemoveUser(ctx context.Context, orgID, teamID, userID string) error +} + +type TeamsAPI struct { + teamsAPI admin.TeamsApi + teamUsersAPI admin.MongoDBCloudUsersApi +} + +func NewTeamsAPIService(teamAPI admin.TeamsApi, userAPI admin.MongoDBCloudUsersApi) *TeamsAPI { + return &TeamsAPI{ + teamsAPI: teamAPI, + teamUsersAPI: userAPI, + } +} + +func (tm *TeamsAPI) ListProjectTeams(ctx context.Context, projectID string) ([]AssignedTeam, error) { + atlasAssignedTeams, _, err := tm.teamsAPI.ListProjectTeams(ctx, projectID).Execute() + if err != nil { + return nil, fmt.Errorf("failed to get project team list from Atlas: %w", err) + } + return TeamRolesFromAtlas(atlasAssignedTeams.GetResults()), err +} + +func (tm *TeamsAPI) GetTeamByName(ctx context.Context, orgID, teamName string) (*AssignedTeam, error) { + atlasTeam, resp, err := tm.teamsAPI.GetTeamByName(ctx, orgID, teamName).Execute() + if err != nil { + if resp.StatusCode == http.StatusNotFound { + return nil, nil + } + return nil, fmt.Errorf("failed to get team by name from Atlas: %w", err) + } + return AssignedTeamFromAtlas(atlasTeam), err +} + +func (tm *TeamsAPI) GetTeamByID(ctx context.Context, orgID, teamID string) (*AssignedTeam, error) { + atlasTeam, _, err := tm.teamsAPI.GetTeamById(ctx, orgID, teamID).Execute() + if err != nil { + return nil, fmt.Errorf("failed to get team by ID from Atlas: %w", err) + } + return AssignedTeamFromAtlas(atlasTeam), err +} + +func (tm *TeamsAPI) Assign(ctx context.Context, at *[]AssignedTeam, projectID string) error { + desiredRoles := TeamRolesToAtlas(*at) + _, _, err := tm.teamsAPI.AddAllTeamsToProject(ctx, projectID, &desiredRoles).Execute() + return err +} + +func (tm *TeamsAPI) Unassign(ctx context.Context, projectID, teamID string) error { + _, err := tm.teamsAPI.RemoveProjectTeam(ctx, projectID, teamID).Execute() + return err +} + +func (tm *TeamsAPI) Create(ctx context.Context, at *Team, orgID string) (*Team, error) { + desiredTeam := TeamToAtlas(at) + atlasTeam, _, err := tm.teamsAPI.CreateTeam(ctx, orgID, desiredTeam).Execute() + if err != nil { + return nil, fmt.Errorf("failed to create team on Atlas: %w", err) + } + + teamResponse := &admin.TeamResponse{} + teamResponse.SetId(atlasTeam.GetId()) + teamResponse.SetName(atlasTeam.GetName()) + return TeamFromAtlas(teamResponse), err +} + +func (tm *TeamsAPI) RenameTeam(ctx context.Context, at *AssignedTeam, orgID, newName string) (*AssignedTeam, error) { + teamUpdate := &admin.TeamUpdate{Name: newName} + atlasTeam, _, err := tm.teamsAPI.RenameTeam(ctx, orgID, at.TeamID, teamUpdate).Execute() + if err != nil { + return nil, fmt.Errorf("failed to rename team on Atlas: %w", err) + } + return AssignedTeamFromAtlas(atlasTeam), err +} + +func (tm *TeamsAPI) UpdateRoles(ctx context.Context, at *AssignedTeam, projectID string, newRoles []akov2.TeamRole) error { + if newRoles == nil { + return nil + } + roles := make([]string, 0, len(newRoles)) + for _, role := range newRoles { + roles = append(roles, string(role)) + } + + _, _, err := tm.teamsAPI.UpdateTeamRoles(ctx, projectID, at.TeamID, &admin.TeamRole{RoleNames: &roles}).Execute() + return err +} + +func (tm *TeamsAPI) GetTeamUsers(ctx context.Context, orgID, teamID string) ([]TeamUser, error) { + atlasUsers, _, err := tm.teamsAPI.ListTeamUsers(ctx, orgID, teamID).Execute() + if err != nil { + return nil, fmt.Errorf("failed to get team users from Atlas: %w", err) + } + + return UsersFromAtlas(atlasUsers), err +} + +func (tm *TeamsAPI) AddUsers(ctx context.Context, usersToAdd *[]TeamUser, orgID, teamID string) error { + _, _, err := tm.teamsAPI.AddTeamUser(ctx, orgID, teamID, UsersToAtlas(usersToAdd)).Execute() + return err +} + +func (tm *TeamsAPI) RemoveUser(ctx context.Context, orgID, teamID, userID string) error { + _, err := tm.teamsAPI.RemoveTeamUser(ctx, orgID, teamID, userID).Execute() + return err +} diff --git a/internal/translation/teams/teams_test.go b/internal/translation/teams/teams_test.go new file mode 100644 index 0000000000..e48cd70a16 --- /dev/null +++ b/internal/translation/teams/teams_test.go @@ -0,0 +1,634 @@ +package teams + +import ( + "context" + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.mongodb.org/atlas-sdk/v20231115008/admin" + "go.mongodb.org/atlas-sdk/v20231115008/mockadmin" + + akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" +) + +const ( + testProjectID = "project-id" + testOrgID = "org-id" +) + +var ( + testTeamName = "team-name" + testTeamID1 = "team1-id" + testTeamID2 = "team2-id" + testUserID = "user-id" +) + +func TestTeamsAPI_ListProjectTeams(t *testing.T) { + ctx := context.Background() + projectID := testProjectID + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedTeams []AssignedTeam + expectedErr error + }{ + { + title: "Should return empty when Atlas is also empty", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().ListProjectTeams(ctx, projectID). + Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). + Return(&admin.PaginatedTeamRole{}, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeams: []AssignedTeam{}, + }, + { + title: "Should return populated team when team is present on Atlas", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().ListProjectTeams(ctx, projectID). + Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). + Return(&admin.PaginatedTeamRole{ + Results: &[]admin.TeamRole{ + { + RoleNames: &[]string{"role1", "role2"}, + TeamId: &testTeamID1, + }, + { + RoleNames: &[]string{"role3", "role4"}, + TeamId: &testTeamID2, + }, + }, + }, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeams: []AssignedTeam{ + { + Roles: []string{"role1", "role2"}, + TeamID: testTeamID1, + }, + { + Roles: []string{"role3", "role4"}, + TeamID: testTeamID2, + }, + }, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().ListProjectTeams(ctx, projectID). + Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). + Return(nil, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: fmt.Errorf("failed to get project team list from Atlas: %w", admin.GenericOpenAPIError{}), + expectedTeams: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + teams, err := ts.ListProjectTeams(ctx, projectID) + require.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedTeams, teams) + }) + } +} + +func TestTeamsAPI_GetTeamByName(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedTeams *AssignedTeam + expectedErr error + }{ + { + title: "Should return team when team is present on Atlas", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().GetTeamByName(ctx, testOrgID, testTeamName). + Return(admin.GetTeamByNameApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().GetTeamByNameExecute(mock.Anything). + Return(&admin.TeamResponse{Id: &testTeamID1, Name: &testTeamName}, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeams: &AssignedTeam{TeamID: testTeamID1, TeamName: testTeamName}, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().GetTeamByName(ctx, testOrgID, testTeamName). + Return(admin.GetTeamByNameApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().GetTeamByNameExecute(mock.Anything). + Return(&admin.TeamResponse{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: fmt.Errorf("failed to get team by name from Atlas: %w", admin.GenericOpenAPIError{}), + expectedTeams: nil, + }, + { + title: "Should return empty team and no error when 404 http error occurs", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().GetTeamByName(ctx, testOrgID, testTeamName). + Return(admin.GetTeamByNameApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().GetTeamByNameExecute(mock.Anything). + Return(&admin.TeamResponse{}, &http.Response{StatusCode: http.StatusNotFound}, admin.GenericOpenAPIError{}) + }, + expectedErr: nil, + expectedTeams: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + teams, err := ts.GetTeamByName(ctx, testOrgID, testTeamName) + require.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedTeams, teams) + }) + } +} + +func TestTeamsAPI_GetTeamByID(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedTeams *AssignedTeam + expectedErr error + }{ + { + title: "Should return team when team is present on Atlas", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().GetTeamById(ctx, testOrgID, testTeamName). + Return(admin.GetTeamByIdApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().GetTeamByIdExecute(mock.Anything). + Return(&admin.TeamResponse{Id: &testTeamID1, Name: &testTeamName}, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeams: &AssignedTeam{TeamID: testTeamID1, TeamName: testTeamName}, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().GetTeamById(ctx, testOrgID, testTeamName). + Return(admin.GetTeamByIdApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().GetTeamByIdExecute(mock.Anything). + Return(&admin.TeamResponse{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: fmt.Errorf("failed to get team by ID from Atlas: %w", admin.GenericOpenAPIError{}), + expectedTeams: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + teams, err := ts.GetTeamByID(ctx, testOrgID, testTeamName) + require.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedTeams, teams) + }) + } +} + +func TestTeamsAPI_Assign(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedErr error + }{ + { + title: "Should assign team to project", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().AddAllTeamsToProject(ctx, mock.Anything, mock.Anything). + Return(admin.AddAllTeamsToProjectApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().AddAllTeamsToProjectExecute(mock.Anything). + Return(&admin.PaginatedTeamRole{ + Results: &[]admin.TeamRole{ + { + RoleNames: &[]string{"role1", "role2"}, + TeamId: &testTeamID1, + }, + }, + }, &http.Response{}, nil) + }, + expectedErr: nil, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().AddAllTeamsToProject(ctx, mock.Anything, mock.Anything). + Return(admin.AddAllTeamsToProjectApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().AddAllTeamsToProjectExecute(mock.Anything). + Return(&admin.PaginatedTeamRole{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: admin.GenericOpenAPIError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + err := ts.Assign(ctx, &[]AssignedTeam{ + { + Roles: []string{"role1", "role2"}, + TeamID: testTeamID1, + TeamName: testTeamName, + }, + }, testProjectID) + require.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestTeamsAPI_Unassign(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedErr error + }{ + { + title: "Should assign team to project", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().RemoveProjectTeam(ctx, mock.Anything, mock.Anything). + Return(admin.RemoveProjectTeamApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().RemoveProjectTeamExecute(mock.Anything). + Return(&http.Response{}, nil) + }, + expectedErr: nil, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().RemoveProjectTeam(ctx, mock.Anything, mock.Anything). + Return(admin.RemoveProjectTeamApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().RemoveProjectTeamExecute(mock.Anything). + Return(&http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: admin.GenericOpenAPIError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + err := ts.Unassign(ctx, mock.Anything, mock.Anything) + require.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestTeamsAPI_Create(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedTeam *Team + expectedErr error + }{ + { + title: "Should create team", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().CreateTeam(ctx, mock.Anything, mock.Anything). + Return(admin.CreateTeamApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().CreateTeamExecute(mock.Anything). + Return(&admin.Team{ + Id: &testTeamID1, + Name: testTeamName, + Usernames: &[]string{"user@name.com"}, + }, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeam: &Team{ + TeamID: testTeamID1, + TeamName: testTeamName, + }, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().CreateTeam(ctx, mock.Anything, mock.Anything). + Return(admin.CreateTeamApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().CreateTeamExecute(mock.Anything). + Return(&admin.Team{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: fmt.Errorf("failed to create team on Atlas: %w", admin.GenericOpenAPIError{}), + expectedTeam: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + team, err := ts.Create(ctx, &Team{}, mock.Anything) + require.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedTeam, team) + }) + } +} + +func TestTeamsAPI_GetTeamUsers(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedTeams []TeamUser + expectedErr error + }{ + { + title: "Should return team when team is present on Atlas", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().ListTeamUsers(ctx, mock.Anything, mock.Anything). + Return(admin.ListTeamUsersApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().ListTeamUsersExecute(mock.Anything). + Return(&admin.PaginatedApiAppUser{ + Results: &[]admin.CloudAppUser{ + { + Username: "user1", + Id: &testUserID, + }, + }, + }, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeams: []TeamUser{ + { + Username: "user1", + UserID: testUserID, + }, + }, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().ListTeamUsers(ctx, mock.Anything, mock.Anything). + Return(admin.ListTeamUsersApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().ListTeamUsersExecute(mock.Anything). + Return(&admin.PaginatedApiAppUser{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: fmt.Errorf("failed to get team users from Atlas: %w", admin.GenericOpenAPIError{}), + expectedTeams: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + teams, err := ts.GetTeamUsers(ctx, mock.Anything, mock.Anything) + require.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedTeams, teams) + }) + } +} + +func TestTeamsAPI_UpdateRoles(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + newRoles []akov2.TeamRole + expectedErr error + }{ + { + title: "should not make API calls when newRole is nil", + mock: func(mockTeamAPI *mockadmin.TeamsApi) {}, + newRoles: nil, + expectedErr: nil, + }, + { + title: "Should successfully update team roles", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().UpdateTeamRoles(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.UpdateTeamRolesApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().UpdateTeamRolesExecute(mock.Anything). + Return(&admin.PaginatedTeamRole{ + Results: &[]admin.TeamRole{ + { + RoleNames: &[]string{"role1", "role2"}, + TeamId: &testTeamID1, + }, + }, + }, &http.Response{}, nil) + }, + newRoles: []akov2.TeamRole{"role1", "role2"}, + expectedErr: nil, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().UpdateTeamRoles(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.UpdateTeamRolesApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().UpdateTeamRolesExecute(mock.Anything). + Return(&admin.PaginatedTeamRole{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + newRoles: []akov2.TeamRole{}, + expectedErr: admin.GenericOpenAPIError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + err := ts.UpdateRoles(ctx, &AssignedTeam{}, mock.Anything, tt.newRoles) + require.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestTeamsAPI_AddUsers(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedErr error + }{ + { + title: "Should successfully add user to team", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().AddTeamUser(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.AddTeamUserApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().AddTeamUserExecute(mock.Anything). + Return(&admin.PaginatedApiAppUser{ + Results: &[]admin.CloudAppUser{ + { + Username: "user1", + Id: &testUserID, + }, + }, + }, &http.Response{}, nil) + }, + expectedErr: nil, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().AddTeamUser(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.AddTeamUserApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().AddTeamUserExecute(mock.Anything). + Return(&admin.PaginatedApiAppUser{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: admin.GenericOpenAPIError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + err := ts.AddUsers(ctx, &[]TeamUser{ + { + Username: "user@name", + UserID: testUserID, + }, + }, mock.Anything, mock.Anything) + require.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestTeamsAPI_RemoveUser(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedErr error + }{ + { + title: "Should successfully remove user from team", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().RemoveTeamUser(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.RemoveTeamUserApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().RemoveTeamUserExecute(mock.Anything). + Return(&http.Response{}, nil) + }, + expectedErr: nil, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().RemoveTeamUser(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.RemoveTeamUserApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().RemoveTeamUserExecute(mock.Anything). + Return(&http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: admin.GenericOpenAPIError{}, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + err := ts.RemoveUser(ctx, mock.Anything, mock.Anything, mock.Anything) + require.Equal(t, tt.expectedErr, err) + }) + } +} + +func TestTeamsAPI_Rename(t *testing.T) { + ctx := context.Background() + + tests := []struct { + title string + mock func(mockTeamAPI *mockadmin.TeamsApi) + expectedTeam *AssignedTeam + expectedErr error + }{ + { + title: "Should successfully rename team", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().RenameTeam(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.RenameTeamApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().RenameTeamExecute(mock.Anything). + Return(&admin.TeamResponse{ + Id: &testTeamID1, + Name: &testTeamName, + }, &http.Response{}, nil) + }, + expectedErr: nil, + expectedTeam: &AssignedTeam{ + TeamID: testTeamID1, + TeamName: testTeamName, + }, + }, + { + title: "Should return error when request fails", + mock: func(mockTeamAPI *mockadmin.TeamsApi) { + mockTeamAPI.EXPECT().RenameTeam(ctx, mock.Anything, mock.Anything, mock.Anything). + Return(admin.RenameTeamApiRequest{ApiService: mockTeamAPI}) + mockTeamAPI.EXPECT().RenameTeamExecute(mock.Anything). + Return(&admin.TeamResponse{}, &http.Response{}, admin.GenericOpenAPIError{}) + }, + expectedErr: fmt.Errorf("failed to rename team on Atlas: %w", admin.GenericOpenAPIError{}), + expectedTeam: nil, + }, + } + + for _, tt := range tests { + t.Run(tt.title, func(t *testing.T) { + mockTeamAPI := mockadmin.NewTeamsApi(t) + tt.mock(mockTeamAPI) + ts := &TeamsAPI{ + teamsAPI: mockTeamAPI, + } + team, err := ts.RenameTeam(ctx, &AssignedTeam{}, mock.Anything, mock.Anything) + require.Equal(t, tt.expectedErr, err) + assert.Equal(t, tt.expectedTeam, team) + }) + } +} diff --git a/pkg/controller/atlasproject/atlasproject_controller.go b/pkg/controller/atlasproject/atlasproject_controller.go index 957d743415..7419f2f2d8 100644 --- a/pkg/controller/atlasproject/atlasproject_controller.go +++ b/pkg/controller/atlasproject/atlasproject_controller.go @@ -37,6 +37,7 @@ import ( "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/pointer" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" @@ -59,6 +60,7 @@ type AtlasProjectReconciler struct { SubObjectDeletionProtection bool projectService project.ProjectService + teamsService teams.TeamsService } // Dev note: duplicate the permissions in both sections below to generate both Role and ClusterRoles @@ -137,7 +139,7 @@ func (r *AtlasProjectReconciler) Reconcile(ctx context.Context, req ctrl.Request } workflowCtx.SdkClient = atlasSdkClient r.projectService = project.NewProjectAPIService(atlasSdkClient.ProjectsApi) - + r.teamsService = teams.NewTeamsAPIService(atlasSdkClient.TeamsApi, atlasSdkClient.MongoDBCloudUsersApi) atlasClient, _, err := r.AtlasProvider.Client(workflowCtx.Context, atlasProject.ConnectionSecretObjectKey(), log) if err != nil { result := workflow.Terminate(workflow.AtlasAPIAccessNotConfigured, err.Error()) diff --git a/pkg/controller/atlasproject/project_test.go b/pkg/controller/atlasproject/project_test.go index 6fbb5ee3f0..9fcb46564a 100644 --- a/pkg/controller/atlasproject/project_test.go +++ b/pkg/controller/atlasproject/project_test.go @@ -3,7 +3,6 @@ package atlasproject import ( "context" "errors" - "net/http" "testing" "github.com/google/go-cmp/cmp" @@ -29,6 +28,7 @@ import ( atlasmocks "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/atlas" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/translation" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/project" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" @@ -45,6 +45,7 @@ func TestHandleProject(t *testing.T) { atlasClientMocker func() *mongodbatlas.Client atlasSDKMocker func() *admin.APIClient projectServiceMocker func() project.ProjectService + teamServiceMocker func() teams.TeamsService interceptors interceptor.Funcs project *akov2.AtlasProject result reconcile.Result @@ -65,6 +66,9 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -97,6 +101,9 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -141,16 +148,10 @@ func TestHandleProject(t *testing.T) { mockPeeringEndpointAPI.EXPECT(). ListPeeringConnectionsExecute(admin.ListPeeringConnectionsApiRequest{ApiService: mockPeeringEndpointAPI}). Return(&admin.PaginatedContainerPeer{}, nil, nil) - mockTeamAPI := mockadmin.NewTeamsApi(t) - mockTeamAPI.EXPECT().ListProjectTeams(context.Background(), mock.Anything). - Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) - mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). - Return(&admin.PaginatedTeamRole{}, &http.Response{}, nil) return &admin.APIClient{ PrivateEndpointServicesApi: mockPrivateEndpointAPI, NetworkPeeringApi: mockPeeringEndpointAPI, - TeamsApi: mockTeamAPI, } }, projectServiceMocker: func() project.ProjectService { @@ -162,6 +163,11 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().ListProjectTeams(context.Background(), mock.Anything).Return([]teams.AssignedTeam{}, nil) + return service + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -189,6 +195,9 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -216,6 +225,9 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, interceptors: interceptor.Funcs{ Patch: func(ctx context.Context, client client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { return errors.New("failed to remove finalizer") @@ -254,6 +266,9 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -336,11 +351,6 @@ func TestHandleProject(t *testing.T) { Return(admin.GetDataProtectionSettingsApiRequest{ApiService: backup}) backup.EXPECT().GetDataProtectionSettingsExecute(mock.AnythingOfType("admin.GetDataProtectionSettingsApiRequest")). Return(nil, nil, nil) - mockTeamAPI := mockadmin.NewTeamsApi(t) - mockTeamAPI.EXPECT().ListProjectTeams(context.Background(), mock.Anything). - Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) - mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). - Return(nil, &http.Response{}, nil) return &admin.APIClient{ ProjectIPAccessListApi: ipAccessList, @@ -350,7 +360,6 @@ func TestHandleProject(t *testing.T) { CustomDatabaseRolesApi: customRoles, ProjectsApi: projectAPI, CloudBackupsApi: backup, - TeamsApi: mockTeamAPI, } }, projectServiceMocker: func() project.ProjectService { @@ -360,6 +369,11 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().ListProjectTeams(context.Background(), mock.Anything).Return([]teams.AssignedTeam{}, nil) + return service + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -438,11 +452,6 @@ func TestHandleProject(t *testing.T) { Return(admin.GetDataProtectionSettingsApiRequest{ApiService: backup}) backup.EXPECT().GetDataProtectionSettingsExecute(mock.AnythingOfType("admin.GetDataProtectionSettingsApiRequest")). Return(nil, nil, nil) - mockTeamAPI := mockadmin.NewTeamsApi(t) - mockTeamAPI.EXPECT().ListProjectTeams(context.Background(), mock.Anything). - Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) - mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). - Return(nil, &http.Response{}, nil) return &admin.APIClient{ ProjectIPAccessListApi: ipAccessList, @@ -452,7 +461,6 @@ func TestHandleProject(t *testing.T) { CustomDatabaseRolesApi: customRoles, ProjectsApi: projectAPI, CloudBackupsApi: backup, - TeamsApi: mockTeamAPI, } }, projectServiceMocker: func() project.ProjectService { @@ -462,6 +470,11 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().ListProjectTeams(context.Background(), mock.Anything).Return([]teams.AssignedTeam{}, nil) + return service + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -542,11 +555,6 @@ func TestHandleProject(t *testing.T) { Return(admin.GetDataProtectionSettingsApiRequest{ApiService: backup}) backup.EXPECT().GetDataProtectionSettingsExecute(mock.AnythingOfType("admin.GetDataProtectionSettingsApiRequest")). Return(nil, nil, nil) - mockTeamAPI := mockadmin.NewTeamsApi(t) - mockTeamAPI.EXPECT().ListProjectTeams(context.Background(), mock.Anything). - Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) - mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). - Return(nil, &http.Response{}, nil) return &admin.APIClient{ ProjectIPAccessListApi: ipAccessList, @@ -556,7 +564,6 @@ func TestHandleProject(t *testing.T) { CustomDatabaseRolesApi: customRoles, ProjectsApi: projectAPI, CloudBackupsApi: backup, - TeamsApi: mockTeamAPI, } }, projectServiceMocker: func() project.ProjectService { @@ -566,6 +573,11 @@ func TestHandleProject(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().ListProjectTeams(context.Background(), mock.Anything).Return([]teams.AssignedTeam{}, nil) + return service + }, project: &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ Name: "my-project", @@ -616,6 +628,7 @@ func TestHandleProject(t *testing.T) { Client: k8sClient, Log: logger, projectService: tt.projectServiceMocker(), + teamsService: tt.teamServiceMocker(), EventRecorder: record.NewFakeRecorder(30), } ctx := &workflow.Context{ @@ -803,6 +816,7 @@ func TestDelete(t *testing.T) { atlasClientMocker func() *mongodbatlas.Client atlasSDKMocker func() *admin.APIClient projectServiceMocker func() project.ProjectService + teamServiceMocker func() teams.TeamsService interceptors interceptor.Funcs objects []client.Object result reconcile.Result @@ -819,6 +833,9 @@ func TestDelete(t *testing.T) { projectServiceMocker: func() project.ProjectService { return nil }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, interceptors: interceptor.Funcs{List: func(ctx context.Context, client client.WithWatch, list client.ObjectList, opts ...client.ListOption) error { return errors.New("failed to list streams instances") }}, @@ -852,6 +869,9 @@ func TestDelete(t *testing.T) { projectServiceMocker: func() project.ProjectService { return nil }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, objects: []client.Object{ &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ @@ -893,6 +913,9 @@ func TestDelete(t *testing.T) { projectServiceMocker: func() project.ProjectService { return nil }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, deletionProtection: true, objects: []client.Object{ &akov2.AtlasProject{ @@ -921,6 +944,9 @@ func TestDelete(t *testing.T) { projectServiceMocker: func() project.ProjectService { return nil }, + teamServiceMocker: func() teams.TeamsService { + return nil + }, objects: []client.Object{ &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ @@ -964,15 +990,9 @@ func TestDelete(t *testing.T) { mockPeeringEndpointAPI.EXPECT(). ListPeeringConnectionsExecute(admin.ListPeeringConnectionsApiRequest{ApiService: mockPeeringEndpointAPI}). Return(&admin.PaginatedContainerPeer{}, nil, nil) - mockTeamAPI := mockadmin.NewTeamsApi(t) - mockTeamAPI.EXPECT().ListProjectTeams(context.Background(), mock.Anything). - Return(admin.ListProjectTeamsApiRequest{ApiService: mockTeamAPI}) - mockTeamAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). - Return(nil, &http.Response{}, nil) return &admin.APIClient{ PrivateEndpointServicesApi: mockPrivateEndpointAPI, NetworkPeeringApi: mockPeeringEndpointAPI, - TeamsApi: mockTeamAPI, } }, projectServiceMocker: func() project.ProjectService { @@ -981,6 +1001,11 @@ func TestDelete(t *testing.T) { return service }, + teamServiceMocker: func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().ListProjectTeams(context.Background(), mock.Anything).Return([]teams.AssignedTeam{}, nil) + return service + }, objects: []client.Object{ &akov2.AtlasProject{ ObjectMeta: metav1.ObjectMeta{ @@ -1057,6 +1082,12 @@ func TestDelete(t *testing.T) { WithInterceptorFuncs(tt.interceptors). Build() logger := zaptest.NewLogger(t).Sugar() + ctx := &workflow.Context{ + Context: context.Background(), + Client: tt.atlasClientMocker(), + SdkClient: tt.atlasSDKMocker(), + Log: logger, + } reconciler := &AtlasProjectReconciler{ Client: k8sClient, ObjectDeletionProtection: tt.deletionProtection, @@ -1067,14 +1098,9 @@ func TestDelete(t *testing.T) { }, }, projectService: tt.projectServiceMocker(), + teamsService: tt.teamServiceMocker(), EventRecorder: record.NewFakeRecorder(1), } - ctx := &workflow.Context{ - Context: context.Background(), - Client: tt.atlasClientMocker(), - SdkClient: tt.atlasSDKMocker(), - Log: logger, - } atlasProject := tt.objects[0].(*akov2.AtlasProject) result, err := reconciler.delete(ctx, "my-org-id", atlasProject) diff --git a/pkg/controller/atlasproject/team_reconciler.go b/pkg/controller/atlasproject/team_reconciler.go index 1979bfdd93..428e1b2339 100644 --- a/pkg/controller/atlasproject/team_reconciler.go +++ b/pkg/controller/atlasproject/team_reconciler.go @@ -4,16 +4,15 @@ import ( "context" "errors" "fmt" - "net/http" "sync" "github.com/google/go-cmp/cmp" - "go.mongodb.org/atlas-sdk/v20231115008/admin" "go.mongodb.org/atlas/mongodbatlas" "golang.org/x/sync/errgroup" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" @@ -67,7 +66,7 @@ func (r *AtlasProjectReconciler) teamReconcile( teamCtx.OrgID = orgID teamCtx.SdkClient = atlasClient - teamID, result := ensureTeamState(teamCtx, team) + teamID, result := r.ensureTeamState(teamCtx, team) if !result.IsOk() { teamCtx.SetConditionFromResult(api.ReadyType, result) if result.IsWarning() { @@ -79,7 +78,7 @@ func (r *AtlasProjectReconciler) teamReconcile( teamCtx.EnsureStatusOption(status.AtlasTeamSetID(teamID)) - result = ensureTeamUsersAreInSync(teamCtx, teamID, team) + result = r.ensureTeamUsersAreInSync(teamCtx, teamID, team) if !result.IsOk() { teamCtx.SetConditionFromResult(api.ReadyType, result) return result.ReconcileResult(), nil @@ -132,53 +131,42 @@ func (r *AtlasProjectReconciler) teamReconcile( } } -func ensureTeamState(workflowCtx *workflow.Context, team *akov2.AtlasTeam) (string, workflow.Result) { - var atlasTeamResponse *admin.TeamResponse +func (r *AtlasProjectReconciler) ensureTeamState(workflowCtx *workflow.Context, team *akov2.AtlasTeam) (string, workflow.Result) { + var atlasAssignedTeam *teams.AssignedTeam var err error if team.Status.ID != "" { - atlasTeamResponse, err = fetchTeamByID(workflowCtx, team.Status.ID) - if err != nil { - return "", workflow.Terminate(workflow.TeamNotCreatedInAtlas, err.Error()) - } - - atlasTeamResponse, err = renameTeam(workflowCtx, atlasTeamResponse, team.Spec.Name) - if err != nil { - return "", workflow.Terminate(workflow.TeamNotUpdatedInAtlas, err.Error()) - } - - return atlasTeamResponse.GetId(), workflow.OK() + atlasAssignedTeam, err = r.fetchTeamByID(workflowCtx, team.Status.ID) + } else { + atlasAssignedTeam, err = r.fetchTeamByName(workflowCtx, team.Spec.Name) } - - atlasTeamResponse, err = fetchTeamByName(workflowCtx, team.Spec.Name) if err != nil { return "", workflow.Terminate(workflow.TeamNotCreatedInAtlas, err.Error()) } - if atlasTeamResponse == nil { - atlasTeam, err := team.ToAtlas() - if err != nil { - return "", workflow.Terminate(workflow.TeamInvalidSpec, err.Error()) + if atlasAssignedTeam == nil { + desiredAtlasTeam := teams.NewTeam(&team.Spec, team.Status.ID) + if desiredAtlasTeam == nil { + return "", workflow.Terminate(workflow.TeamInvalidSpec, "teamspec is invalid") } - atlasTeam, err = createTeam(workflowCtx, atlasTeam) + atlasTeam, err := r.createTeam(workflowCtx, desiredAtlasTeam) if err != nil { return "", workflow.Terminate(workflow.TeamNotCreatedInAtlas, err.Error()) } - - return atlasTeam.GetId(), workflow.OK() + return atlasTeam.TeamID, workflow.OK() } - atlasTeamResponse, err = renameTeam(workflowCtx, atlasTeamResponse, team.Spec.Name) + atlasAssignedTeam, err = r.renameTeam(workflowCtx, atlasAssignedTeam, team.Spec.Name) if err != nil { return "", workflow.Terminate(workflow.TeamNotUpdatedInAtlas, err.Error()) } - return atlasTeamResponse.GetId(), workflow.OK() + return atlasAssignedTeam.TeamID, workflow.OK() } -func ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team *akov2.AtlasTeam) workflow.Result { - atlasUsers, _, err := workflowCtx.SdkClient.TeamsApi.ListTeamUsers(workflowCtx.Context, workflowCtx.OrgID, teamID).Execute() +func (r *AtlasProjectReconciler) ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team *akov2.AtlasTeam) workflow.Result { + atlasUsers, err := r.teamsService.GetTeamUsers(workflowCtx.Context, workflowCtx.OrgID, teamID) if err != nil { return workflow.Terminate(workflow.TeamUsersNotReady, err.Error()) } @@ -188,22 +176,20 @@ func ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team usernamesMap[string(username)] = struct{}{} } - atlasUsernamesMap := map[string]admin.CloudAppUser{} - for _, atlasUser := range atlasUsers.GetResults() { + atlasUsernamesMap := map[string]teams.TeamUser{} + for _, atlasUser := range atlasUsers { atlasUsernamesMap[atlasUser.Username] = atlasUser } g, taskContext := errgroup.WithContext(workflowCtx.Context) - if atlasUsers.Results != nil { - for _, user := range atlasUsers.GetResults() { - if _, ok := usernamesMap[user.Username]; !ok { - g.Go(func() error { - workflowCtx.Log.Debugf("removing user %s from team %s", user.GetId(), teamID) - _, err := workflowCtx.SdkClient.TeamsApi.RemoveTeamUser(taskContext, workflowCtx.OrgID, teamID, user.GetId()).Execute() - return err - }) - } + for _, user := range atlasUsers { + if _, ok := usernamesMap[user.Username]; !ok { + g.Go(func() error { + workflowCtx.Log.Debugf("removing user %s from team %s", user.UserID, teamID) + err := r.teamsService.RemoveUser(workflowCtx.Context, workflowCtx.OrgID, teamID, user.UserID) + return err + }) } } @@ -214,7 +200,7 @@ func ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team } g, taskContext = errgroup.WithContext(workflowCtx.Context) - toAdd := make([]admin.AddUserToTeam, 0, len(team.Spec.Usernames)) + toAdd := make([]teams.TeamUser, 0, len(team.Spec.Usernames)) lock := sync.Mutex{} for i := range team.Spec.Usernames { username := team.Spec.Usernames[i] @@ -227,7 +213,7 @@ func ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team } lock.Lock() - toAdd = append(toAdd, admin.AddUserToTeam{Id: user.GetId()}) + toAdd = append(toAdd, teams.TeamUser{UserID: user.GetId()}) lock.Unlock() return nil @@ -246,7 +232,7 @@ func ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team } workflowCtx.Log.Debugf("Adding users to team %s", teamID) - _, _, err = workflowCtx.SdkClient.TeamsApi.AddTeamUser(workflowCtx.Context, workflowCtx.OrgID, teamID, &toAdd).Execute() + err = r.teamsService.AddUsers(workflowCtx.Context, &toAdd, workflowCtx.OrgID, teamID) if err != nil { return workflow.Terminate(workflow.TeamUsersNotReady, err.Error()) } @@ -254,55 +240,49 @@ func ensureTeamUsersAreInSync(workflowCtx *workflow.Context, teamID string, team return workflow.OK() } -func fetchTeamByID(workflowCtx *workflow.Context, teamID string) (*admin.TeamResponse, error) { +func (r *AtlasProjectReconciler) fetchTeamByID(workflowCtx *workflow.Context, teamID string) (*teams.AssignedTeam, error) { workflowCtx.Log.Debugf("fetching team %s from atlas", teamID) - atlasTeamResponse, _, err := workflowCtx.SdkClient.TeamsApi.GetTeamById(workflowCtx.Context, workflowCtx.OrgID, teamID).Execute() + atlasTeam, err := r.teamsService.GetTeamByID(workflowCtx.Context, workflowCtx.OrgID, teamID) if err != nil { return nil, err } - return atlasTeamResponse, nil + return atlasTeam, nil } -func fetchTeamByName(workflowCtx *workflow.Context, teamName string) (*admin.TeamResponse, error) { +func (r *AtlasProjectReconciler) fetchTeamByName(workflowCtx *workflow.Context, teamName string) (*teams.AssignedTeam, error) { workflowCtx.Log.Debugf("fetching team named %s from atlas", teamName) - atlasTeamResponse, resp, err := workflowCtx.SdkClient.TeamsApi.GetTeamByName(workflowCtx.Context, workflowCtx.OrgID, teamName).Execute() + atlasTeam, err := r.teamsService.GetTeamByName(workflowCtx.Context, workflowCtx.OrgID, teamName) if err != nil { - if resp.StatusCode == http.StatusNotFound { - return nil, nil - } - return nil, err } - return atlasTeamResponse, nil + return atlasTeam, nil } -func createTeam(workflowCtx *workflow.Context, atlasTeam *admin.Team) (*admin.Team, error) { - workflowCtx.Log.Debugf("create team named %s in atlas", atlasTeam.Name) - atlasTeam, _, err := workflowCtx.SdkClient.TeamsApi.CreateTeam(workflowCtx.Context, workflowCtx.OrgID, atlasTeam).Execute() +func (r *AtlasProjectReconciler) createTeam(workflowCtx *workflow.Context, desiredAtlasTeam *teams.Team) (*teams.Team, error) { + workflowCtx.Log.Debugf("create team named %s in atlas", desiredAtlasTeam.TeamName) + atlasTeam, err := r.teamsService.Create(workflowCtx.Context, desiredAtlasTeam, workflowCtx.OrgID) if err != nil { return nil, err } return atlasTeam, nil } -func renameTeam(workflowCtx *workflow.Context, atlasTeamResponse *admin.TeamResponse, newName string) (*admin.TeamResponse, error) { - if atlasTeamResponse.GetName() == newName { - return atlasTeamResponse, nil +func (r *AtlasProjectReconciler) renameTeam(workflowCtx *workflow.Context, at *teams.AssignedTeam, newName string) (*teams.AssignedTeam, error) { + if at.TeamName == newName { + return at, nil } - - workflowCtx.Log.Debugf("updating name of team %s in atlas", atlasTeamResponse.GetId()) - teamUpdate := admin.TeamUpdate{Name: newName} - atlasTeamResponse, _, err := workflowCtx.SdkClient.TeamsApi.RenameTeam(workflowCtx.Context, workflowCtx.OrgID, atlasTeamResponse.GetId(), &teamUpdate).Execute() + workflowCtx.Log.Debugf("updating name of team %s in atlas", at.TeamID) + atlasTeam, err := r.teamsService.RenameTeam(workflowCtx.Context, at, workflowCtx.OrgID, newName) if err != nil { return nil, err } - return atlasTeamResponse, nil + return atlasTeam, nil } -func teamsManagedByAtlas(workflowCtx *workflow.Context) customresource.AtlasChecker { +func (r *AtlasProjectReconciler) teamsManagedByAtlas(workflowCtx *workflow.Context) customresource.AtlasChecker { return func(resource api.AtlasCustomResource) (bool, error) { team, ok := resource.(*akov2.AtlasTeam) if !ok { @@ -313,7 +293,7 @@ func teamsManagedByAtlas(workflowCtx *workflow.Context) customresource.AtlasChec return false, nil } - atlasTeam, _, err := workflowCtx.SdkClient.TeamsApi.GetTeamById(workflowCtx.Context, workflowCtx.OrgID, team.Status.ID).Execute() + atlasTeam, err := r.teamsService.GetTeamByID(workflowCtx.Context, workflowCtx.OrgID, team.Status.ID) if err != nil { var apiError *mongodbatlas.ErrorResponse if errors.As(err, &apiError) && (apiError.ErrorCode == atlas.NotInGroup || apiError.ErrorCode == atlas.ResourceNotFound) { @@ -323,12 +303,12 @@ func teamsManagedByAtlas(workflowCtx *workflow.Context) customresource.AtlasChec return false, err } - atlasTeamUsers, _, err := workflowCtx.SdkClient.TeamsApi.ListTeamUsers(workflowCtx.Context, workflowCtx.OrgID, atlasTeam.GetName()).Execute() + atlasTeamUsers, err := r.teamsService.GetTeamUsers(workflowCtx.Context, workflowCtx.OrgID, team.Status.ID) if err != nil { return false, err } - if team.Spec.Name != atlasTeam.GetName() || len(atlasTeamUsers.GetResults()) == 0 { + if len(atlasTeamUsers) == 0 || team.Spec.Name != atlasTeam.TeamName { return false, err } @@ -337,8 +317,8 @@ func teamsManagedByAtlas(workflowCtx *workflow.Context) customresource.AtlasChec usernames = append(usernames, string(username)) } - atlasUsernames := make([]string, 0, len(atlasTeamUsers.GetResults())) - for _, user := range atlasTeamUsers.GetResults() { + atlasUsernames := make([]string, 0, len(atlasTeamUsers)) + for _, user := range atlasTeamUsers { atlasUsernames = append(atlasUsernames, user.Username) } diff --git a/pkg/controller/atlasproject/team_reconciler_test.go b/pkg/controller/atlasproject/team_reconciler_test.go index 7de2fb7086..71fbf51888 100644 --- a/pkg/controller/atlasproject/team_reconciler_test.go +++ b/pkg/controller/atlasproject/team_reconciler_test.go @@ -3,15 +3,14 @@ package atlasproject import ( "context" "errors" - "net/http" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "go.mongodb.org/atlas-sdk/v20231115008/admin" - "go.mongodb.org/atlas-sdk/v20231115008/mockadmin" "go.mongodb.org/atlas/mongodbatlas" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/translation" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/controller/atlas" @@ -19,13 +18,14 @@ import ( ) func TestTeamManagedByAtlas(t *testing.T) { + var r AtlasProjectReconciler t.Run("should return error when passing wrong resource", func(t *testing.T) { workflowCtx := &workflow.Context{ OrgID: "orgID", SdkClient: &admin.APIClient{}, Context: context.Background(), } - checker := teamsManagedByAtlas(workflowCtx) + checker := r.teamsManagedByAtlas(workflowCtx) result, err := checker(&akov2.AtlasProject{}) assert.EqualError(t, err, "failed to match resource type as AtlasTeams") assert.False(t, result) @@ -37,23 +37,14 @@ func TestTeamManagedByAtlas(t *testing.T) { SdkClient: &admin.APIClient{}, Context: context.Background(), } - checker := teamsManagedByAtlas(workflowCtx) + checker := r.teamsManagedByAtlas(workflowCtx) result, err := checker(&akov2.AtlasTeam{}) assert.NoError(t, err) assert.False(t, result) }) t.Run("should return false when resource was not found in Atlas", func(t *testing.T) { - atlasClient := admin.APIClient{ - TeamsApi: func() *mockadmin.TeamsApi { - TeamsAPI := mockadmin.NewTeamsApi(t) - TeamsAPI.EXPECT().GetTeamById(context.Background(), "orgID", "team-id-1"). - Return(admin.GetTeamByIdApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().GetTeamByIdExecute(mock.Anything). - Return(nil, &http.Response{}, &mongodbatlas.ErrorResponse{ErrorCode: atlas.ResourceNotFound}) - return TeamsAPI - }(), - } + atlasClient := admin.APIClient{} team := &akov2.AtlasTeam{ Status: status.TeamStatus{ ID: "team-id-1", @@ -64,23 +55,21 @@ func TestTeamManagedByAtlas(t *testing.T) { SdkClient: &atlasClient, Context: context.Background(), } - checker := teamsManagedByAtlas(workflowCtx) + teamService := func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().GetTeamByID(workflowCtx.Context, workflowCtx.OrgID, "team-id-1"). + Return(nil, &mongodbatlas.ErrorResponse{ErrorCode: atlas.ResourceNotFound}) + return service + } + r = AtlasProjectReconciler{teamsService: teamService()} + checker := r.teamsManagedByAtlas(workflowCtx) result, err := checker(team) assert.NoError(t, err) assert.False(t, result) }) t.Run("should return error when failed to fetch the team from Atlas", func(t *testing.T) { - atlasClient := admin.APIClient{ - TeamsApi: func() *mockadmin.TeamsApi { - TeamsAPI := mockadmin.NewTeamsApi(t) - TeamsAPI.EXPECT().GetTeamById(context.Background(), "orgID", "team-id-1"). - Return(admin.GetTeamByIdApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().GetTeamByIdExecute(mock.Anything). - Return(nil, &http.Response{}, errors.New("unavailable")) - return TeamsAPI - }(), - } + atlasClient := admin.APIClient{} team := &akov2.AtlasTeam{ Status: status.TeamStatus{ ID: "team-id-1", @@ -91,42 +80,21 @@ func TestTeamManagedByAtlas(t *testing.T) { SdkClient: &atlasClient, Context: context.Background(), } - checker := teamsManagedByAtlas(workflowCtx) + teamService := func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().GetTeamByID(workflowCtx.Context, workflowCtx.OrgID, "team-id-1"). + Return(nil, errors.New("unavailable")) + return service + } + r = AtlasProjectReconciler{teamsService: teamService()} + checker := r.teamsManagedByAtlas(workflowCtx) result, err := checker(team) assert.EqualError(t, err, "unavailable") assert.False(t, result) }) t.Run("should return false when resource are equal", func(t *testing.T) { - atlasClient := admin.APIClient{ - TeamsApi: func() *mockadmin.TeamsApi { - TeamsAPI := mockadmin.NewTeamsApi(t) - TeamsAPI.EXPECT().GetTeamById(context.Background(), "orgID-1", "team-id-1"). - Return(admin.GetTeamByIdApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().GetTeamByIdExecute(mock.Anything). - Return(&admin.TeamResponse{ - Id: func(s string) *string { return &s }("team-id-1"), - Links: nil, - Name: func(s string) *string { return &s }("My Team"), - }, &http.Response{}, nil) - TeamsAPI.EXPECT().ListTeamUsers(context.Background(), "orgID-1", "My Team"). - Return(admin.ListTeamUsersApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().ListTeamUsersExecute(mock.Anything). - Return(&admin.PaginatedApiAppUser{ - Links: nil, - Results: &[]admin.CloudAppUser{ - { - Username: "user1@mongodb.com", - }, - { - Username: "user2@mongodb.com", - }, - }, - TotalCount: nil, - }, &http.Response{}, nil) - return TeamsAPI - }(), - } + atlasClient := admin.APIClient{} team := &akov2.AtlasTeam{ Spec: akov2.TeamSpec{ Name: "My Team", @@ -141,42 +109,34 @@ func TestTeamManagedByAtlas(t *testing.T) { SdkClient: &atlasClient, Context: context.Background(), } - checker := teamsManagedByAtlas(workflowCtx) + teamService := func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().GetTeamByID(workflowCtx.Context, workflowCtx.OrgID, "team-id-1"). + Return(&teams.AssignedTeam{ + TeamID: "team-id-1", + TeamName: "My Team", + Roles: nil, + }, nil) + service.EXPECT().GetTeamUsers(workflowCtx.Context, workflowCtx.OrgID, "team-id-1"). + Return([]teams.TeamUser{ + { + Username: "user1@mongodb.com", + }, + { + Username: "user2@mongodb.com", + }, + }, nil) + return service + } + r = AtlasProjectReconciler{teamsService: teamService()} + checker := r.teamsManagedByAtlas(workflowCtx) result, err := checker(team) assert.NoError(t, err) assert.False(t, result) }) t.Run("should return true when resource are different", func(t *testing.T) { - atlasClient := admin.APIClient{ - TeamsApi: func() *mockadmin.TeamsApi { - TeamsAPI := mockadmin.NewTeamsApi(t) - TeamsAPI.EXPECT().GetTeamById(context.Background(), "orgID-1", "team-id-1"). - Return(admin.GetTeamByIdApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().GetTeamByIdExecute(mock.Anything). - Return(&admin.TeamResponse{ - Id: func(s string) *string { return &s }("team-id-1"), - Links: nil, - Name: func(s string) *string { return &s }("My Team"), - }, &http.Response{}, nil) - TeamsAPI.EXPECT().ListTeamUsers(context.Background(), "orgID-1", "My Team"). - Return(admin.ListTeamUsersApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().ListTeamUsersExecute(mock.Anything). - Return(&admin.PaginatedApiAppUser{ - Links: nil, - Results: &[]admin.CloudAppUser{ - { - Username: "user1@mongodb.com", - }, - { - Username: "user2@mongodb.com", - }, - }, - TotalCount: nil, - }, &http.Response{}, nil) - return TeamsAPI - }(), - } + atlasClient := admin.APIClient{} team := &akov2.AtlasTeam{ Spec: akov2.TeamSpec{ Name: "My Team", @@ -191,7 +151,27 @@ func TestTeamManagedByAtlas(t *testing.T) { SdkClient: &atlasClient, Context: context.Background(), } - checker := teamsManagedByAtlas(workflowCtx) + teamService := func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().GetTeamByID(workflowCtx.Context, workflowCtx.OrgID, "team-id-1"). + Return(&teams.AssignedTeam{ + TeamID: "team-id-1", + TeamName: "My Team", + Roles: nil, + }, nil) + service.EXPECT().GetTeamUsers(workflowCtx.Context, workflowCtx.OrgID, "team-id-1"). + Return([]teams.TeamUser{ + { + Username: "user1@mongodb.com", + }, + { + Username: "user2@mongodb.com", + }, + }, nil) + return service + } + r = AtlasProjectReconciler{teamsService: teamService()} + checker := r.teamsManagedByAtlas(workflowCtx) result, err := checker(team) assert.NoError(t, err) assert.True(t, result) diff --git a/pkg/controller/atlasproject/teams.go b/pkg/controller/atlasproject/teams.go index 9c33275b02..6e043f11b9 100644 --- a/pkg/controller/atlasproject/teams.go +++ b/pkg/controller/atlasproject/teams.go @@ -1,11 +1,11 @@ package atlasproject import ( - "go.mongodb.org/atlas-sdk/v20231115008/admin" "k8s.io/apimachinery/pkg/types" controllerruntime "sigs.k8s.io/controller-runtime" "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/kube" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" @@ -67,7 +67,8 @@ func (r *AtlasProjectReconciler) ensureAssignedTeams(workflowCtx *workflow.Conte func (r *AtlasProjectReconciler) syncAssignedTeams(ctx *workflow.Context, projectID string, project *akov2.AtlasProject, teamsToAssign map[string]*akov2.Team) error { ctx.Log.Debug("fetching assigned teams from atlas") - atlasAssignedTeams, _, err := ctx.SdkClient.TeamsApi.ListProjectTeams(ctx.Context, projectID).Execute() + + atlasAssignedTeams, err := r.teamsService.ListProjectTeams(ctx.Context, projectID) if err != nil { return err } @@ -80,76 +81,73 @@ func (r *AtlasProjectReconciler) syncAssignedTeams(ctx *workflow.Context, projec defer statushandler.Update(ctx, r.Client, r.EventRecorder, project) - toDelete := make([]*admin.TeamRole, 0, len(atlasAssignedTeams.GetResults())) + toDelete := make([]*teams.AssignedTeam, 0, len(atlasAssignedTeams)) - if atlasAssignedTeams != nil && atlasAssignedTeams.Results != nil { - for _, atlasAssignedTeam := range atlasAssignedTeams.GetResults() { - if atlasAssignedTeam.GetTeamId() == "" { - continue - } + for _, atlasAssignedTeam := range atlasAssignedTeams { + if atlasAssignedTeam.TeamID == "" { + continue + } - desiredTeam, ok := teamsToAssign[atlasAssignedTeam.GetTeamId()] - if !ok { - result := atlasAssignedTeam - toDelete = append(toDelete, &result) + desiredTeam, ok := teamsToAssign[atlasAssignedTeam.TeamID] + if !ok { + toDelete = append(toDelete, &atlasAssignedTeam) + continue + } - continue + if !hasTeamRolesChanged(atlasAssignedTeam.Roles, desiredTeam.Roles) { + currentProjectsStatus[atlasAssignedTeam.TeamID] = status.ProjectTeamStatus{ + ID: atlasAssignedTeam.TeamID, + TeamRef: desiredTeam.TeamRef, } + delete(teamsToAssign, atlasAssignedTeam.TeamID) - if !hasTeamRolesChanged(atlasAssignedTeam.GetRoleNames(), desiredTeam.Roles) { - currentProjectsStatus[atlasAssignedTeam.GetTeamId()] = status.ProjectTeamStatus{ - ID: atlasAssignedTeam.GetTeamId(), - TeamRef: desiredTeam.TeamRef, - } - delete(teamsToAssign, atlasAssignedTeam.GetTeamId()) - - continue - } + continue + } - ctx.Log.Debugf("removing team %s from project for later update", atlasAssignedTeam.GetTeamId()) - _, err = ctx.SdkClient.TeamsApi.RemoveProjectTeam(ctx.Context, projectID, atlasAssignedTeam.GetTeamId()).Execute() - if err != nil { - ctx.Log.Warnf("failed to remove team %s from project: %s", atlasAssignedTeam.GetTeamId(), err.Error()) - } + ctx.Log.Debugf("removing team %s from project for later update", atlasAssignedTeam.TeamID) + err := r.teamsService.Unassign(ctx.Context, projectID, atlasAssignedTeam.TeamID) + if err != nil { + ctx.Log.Warnf("failed to remove team %s from project: %s", atlasAssignedTeam.TeamID, err.Error()) } } for _, atlasAssignedTeam := range toDelete { - ctx.Log.Debugf("removing team %s from project", atlasAssignedTeam.GetTeamId()) - _, err = ctx.SdkClient.TeamsApi.RemoveProjectTeam(ctx.Context, projectID, atlasAssignedTeam.GetTeamId()).Execute() + ctx.Log.Debugf("removing team %s from project", atlasAssignedTeam.TeamID) + err := r.teamsService.Unassign(ctx.Context, projectID, atlasAssignedTeam.TeamID) if err != nil { - ctx.Log.Warnf("failed to remove team %s from project: %s", atlasAssignedTeam.GetTeamId(), err.Error()) + ctx.Log.Warnf("failed to remove team %s from project: %s", atlasAssignedTeam.TeamID, err.Error()) } - teamRef := getTeamRefFromProjectStatus(project, atlasAssignedTeam.GetTeamId()) + teamRef := getTeamRefFromProjectStatus(project, atlasAssignedTeam.TeamID) if teamRef == nil { - ctx.Log.Warnf("unable to find team %s status in the project", atlasAssignedTeam.GetTeamId()) + ctx.Log.Warnf("unable to find team %s status in the project", atlasAssignedTeam.TeamID) } else { if err = r.updateTeamState(ctx, project, teamRef, true); err != nil { - ctx.Log.Warnf("failed to update team %s status with removed project: %s", atlasAssignedTeam.GetTeamId(), err.Error()) + ctx.Log.Warnf("failed to update team %s status with removed project: %s", atlasAssignedTeam.TeamID, err.Error()) } } - delete(currentProjectsStatus, atlasAssignedTeam.GetTeamId()) + delete(currentProjectsStatus, atlasAssignedTeam.TeamID) } if len(teamsToAssign) > 0 { ctx.Log.Debug("assigning teams to project") - projectTeams := make([]admin.TeamRole, 0, len(teamsToAssign)) + projectTeams := make([]teams.AssignedTeam, 0, len(teamsToAssign)) for teamID := range teamsToAssign { - assignedTeam := teamsToAssign[teamID] - projectTeams = append(projectTeams, assignedTeam.ToAtlas(teamID)) + teamToAssign := teams.NewAssignedTeam(teamsToAssign[teamID], teamID) + + projectTeams = append(projectTeams, *teamToAssign) currentProjectsStatus[teamID] = status.ProjectTeamStatus{ ID: teamID, - TeamRef: assignedTeam.TeamRef, + TeamRef: teamsToAssign[teamID].TeamRef, } - if err = r.updateTeamState(ctx, project, &assignedTeam.TeamRef, false); err != nil { + if err = r.updateTeamState(ctx, project, &teamsToAssign[teamID].TeamRef, false); err != nil { ctx.Log.Warnf("failed to update team %s status with added project: %s", teamID, err.Error()) } } - _, _, err = ctx.SdkClient.TeamsApi.AddAllTeamsToProject(ctx.Context, projectID, &projectTeams).Execute() + err = r.teamsService.Assign(ctx.Context, &projectTeams, projectID) if err != nil { return err } diff --git a/pkg/controller/atlasproject/teams_test.go b/pkg/controller/atlasproject/teams_test.go index df337bc22d..7229108f8f 100644 --- a/pkg/controller/atlasproject/teams_test.go +++ b/pkg/controller/atlasproject/teams_test.go @@ -2,13 +2,10 @@ package atlasproject import ( "context" - "net/http" "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "go.mongodb.org/atlas-sdk/v20231115008/admin" - "go.mongodb.org/atlas-sdk/v20231115008/mockadmin" "go.uber.org/zap/zaptest" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,6 +14,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/fake" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/mocks/translation" + "github.com/mongodb/mongodb-atlas-kubernetes/v2/internal/translation/teams" akov2 "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/common" "github.com/mongodb/mongodb-atlas-kubernetes/v2/pkg/api/v1/status" @@ -150,59 +149,48 @@ func TestSyncAssignedTeams(t *testing.T) { WithStatusSubresource(project, team1, team2, team3). Build() - atlasClient := &admin.APIClient{ - TeamsApi: func() *mockadmin.TeamsApi { - TeamsAPI := mockadmin.NewTeamsApi(t) - TeamsAPI.EXPECT().ListProjectTeams(nil, "projectID"). - Return(admin.ListProjectTeamsApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().ListProjectTeamsExecute(mock.Anything). - Return(&admin.PaginatedTeamRole{ - Links: nil, - Results: &[]admin.TeamRole{ - { - Links: nil, - RoleNames: &[]string{"GROUP_OWNER"}, - TeamId: &team1.Status.ID, - }, - { - Links: nil, - RoleNames: &[]string{"GROUP_OWNER"}, - TeamId: &team2.Status.ID, - }, - { - Links: nil, - RoleNames: &[]string{"GROUP_READ_ONLY"}, - TeamId: &team3.Status.ID, - }, - }, - TotalCount: nil, - }, &http.Response{}, nil) - TeamsAPI.EXPECT().RemoveProjectTeam(nil, "projectID", "teamID_2"). - Return(admin.RemoveProjectTeamApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().RemoveProjectTeamExecute(mock.Anything). - Return(nil, nil) - TeamsAPI.EXPECT().RemoveProjectTeam(nil, "projectID", "teamID_3"). - Return(admin.RemoveProjectTeamApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().RemoveProjectTeamExecute(mock.Anything). - Return(nil, nil) - TeamsAPI.EXPECT().AddAllTeamsToProject(nil, "projectID", &[]admin.TeamRole{{Links: nil, RoleNames: &[]string{"GROUP_READ_ONLY"}, TeamId: &team2.Status.ID}}). - Return(admin.AddAllTeamsToProjectApiRequest{ApiService: TeamsAPI}) - TeamsAPI.EXPECT().AddAllTeamsToProjectExecute(mock.Anything).Return(&admin.PaginatedTeamRole{}, &http.Response{}, nil) - return TeamsAPI - }(), - } - + atlasClient := &admin.APIClient{} logger := zaptest.NewLogger(t).Sugar() ctx := &workflow.Context{ Log: logger, SdkClient: atlasClient, } + teamService := func() teams.TeamsService { + service := translation.NewTeamsServiceMock(t) + service.EXPECT().ListProjectTeams(nil, "projectID").Return([]teams.AssignedTeam{ + { + Roles: []string{"GROUP_OWNER"}, + TeamID: team1.Status.ID, + TeamName: "teamName_1", + }, + { + Roles: []string{"GROUP_OWNER"}, + TeamID: team2.Status.ID, + TeamName: "teamName_2", + }, + { + Roles: []string{"GROUP_READ_ONLY"}, + TeamID: team3.Status.ID, + TeamName: "teamName_3", + }, + }, nil) + service.EXPECT().Unassign(nil, "projectID", "teamID_2").Return(nil) + service.EXPECT().Unassign(nil, "projectID", "teamID_3").Return(nil) + service.EXPECT().Assign(nil, + &[]teams.AssignedTeam{ + { + Roles: []string{"GROUP_READ_ONLY"}, + TeamID: "teamID_2", + }, + }, "projectID").Return(nil) + return service + } r := &AtlasProjectReconciler{ Client: k8sClient, EventRecorder: record.NewFakeRecorder(10), Log: logger, + teamsService: teamService(), } - err := r.syncAssignedTeams(ctx, "projectID", project, tt.teamsToAssign) assert.Equal(t, tt.expectedErr, err) })