From af5d98e182971ccb59e75c8338f53a02cd69d707 Mon Sep 17 00:00:00 2001 From: Keepers Date: Wed, 23 Aug 2023 10:46:28 -0600 Subject: [PATCH] Cleanup services (#4059) Some quick tech-debt splitting up of the code in services/m365 --- #### Does this PR need a docs update or release note? - [x] :no_entry: No #### Type of change - [x] :broom: Tech Debt/Cleanup #### Test Plan - [x] :zap: Unit test - [x] :green_heart: E2E --- src/pkg/services/m365/m365.go | 291 ----------- src/pkg/services/m365/sites.go | 99 ++++ src/pkg/services/m365/sites_test.go | 191 +++++++ src/pkg/services/m365/users.go | 211 ++++++++ .../m365/{m365_test.go => users_test.go} | 479 ++++++------------ 5 files changed, 653 insertions(+), 618 deletions(-) create mode 100644 src/pkg/services/m365/sites.go create mode 100644 src/pkg/services/m365/sites_test.go create mode 100644 src/pkg/services/m365/users.go rename src/pkg/services/m365/{m365_test.go => users_test.go} (64%) diff --git a/src/pkg/services/m365/m365.go b/src/pkg/services/m365/m365.go index 469f4d08f..6bb8125c4 100644 --- a/src/pkg/services/m365/m365.go +++ b/src/pkg/services/m365/m365.go @@ -6,9 +6,6 @@ import ( "github.com/alcionai/clues" "github.com/microsoftgraph/msgraph-sdk-go/models" - "github.com/alcionai/corso/src/internal/common/idname" - "github.com/alcionai/corso/src/internal/common/ptr" - "github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/fault" @@ -28,294 +25,6 @@ type getAller[T any] interface { GetAll(ctx context.Context, errs *fault.Bus) ([]T, error) } -// --------------------------------------------------------------------------- -// Users -// --------------------------------------------------------------------------- - -// User is the minimal information required to identify and display a user. -type User struct { - PrincipalName string - ID string - Name string - Info api.UserInfo -} - -// UserNoInfo is the minimal information required to identify and display a user. -// TODO: Remove this once `UsersCompatNoInfo` is removed -type UserNoInfo struct { - PrincipalName string - ID string - Name string -} - -// UsersCompat returns a list of users in the specified M365 tenant. -// TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault -// package. -func UsersCompat(ctx context.Context, acct account.Account) ([]*User, error) { - errs := fault.New(true) - - us, err := Users(ctx, acct, errs) - if err != nil { - return nil, err - } - - return us, errs.Failure() -} - -// UsersCompatNoInfo returns a list of users in the specified M365 tenant. -// TODO: Remove this once `Info` is removed from the `User` struct and callers -// have switched over -func UsersCompatNoInfo(ctx context.Context, acct account.Account) ([]*UserNoInfo, error) { - errs := fault.New(true) - - us, err := usersNoInfo(ctx, acct, errs) - if err != nil { - return nil, err - } - - return us, errs.Failure() -} - -// UserHasMailbox returns true if the user has an exchange mailbox enabled -// false otherwise, and a nil pointer and an error in case of error -func UserHasMailbox(ctx context.Context, acct account.Account, userID string) (bool, error) { - ac, err := makeAC(ctx, acct, path.ExchangeService) - if err != nil { - return false, clues.Stack(err).WithClues(ctx) - } - - _, err = ac.Users().GetMailInbox(ctx, userID) - if err != nil { - if err := api.EvaluateMailboxError(err); err != nil { - return false, clues.Stack(err) - } - - return false, nil - } - - return true, nil -} - -// UserHasDrives returns true if the user has any drives -// false otherwise, and a nil pointer and an error in case of error -func UserHasDrives(ctx context.Context, acct account.Account, userID string) (bool, error) { - ac, err := makeAC(ctx, acct, path.OneDriveService) - if err != nil { - return false, clues.Stack(err).WithClues(ctx) - } - - return checkUserHasDrives(ctx, ac.Users(), userID) -} - -func checkUserHasDrives(ctx context.Context, dgdd getDefaultDriver, userID string) (bool, error) { - _, err := dgdd.GetDefaultDrive(ctx, userID) - if err != nil { - // we consider this a non-error case, since it - // answers the question the caller is asking. - if clues.HasLabel(err, graph.LabelsMysiteNotFound) || clues.HasLabel(err, graph.LabelsNoSharePointLicense) { - return false, nil - } - - if graph.IsErrUserNotFound(err) { - return false, clues.Stack(graph.ErrResourceOwnerNotFound, err) - } - - return false, clues.Stack(err) - } - - return true, nil -} - -// usersNoInfo returns a list of users in the specified M365 tenant - with no info -// TODO: Remove this once we remove `Info` from `Users` and instead rely on the `GetUserInfo` API -// to get user information -func usersNoInfo(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*UserNoInfo, error) { - ac, err := makeAC(ctx, acct, path.UnknownService) - if err != nil { - return nil, clues.Stack(err).WithClues(ctx) - } - - us, err := ac.Users().GetAll(ctx, errs) - if err != nil { - return nil, err - } - - ret := make([]*UserNoInfo, 0, len(us)) - - for _, u := range us { - pu, err := parseUser(u) - if err != nil { - return nil, clues.Wrap(err, "formatting user data") - } - - puNoInfo := &UserNoInfo{ - PrincipalName: pu.PrincipalName, - ID: pu.ID, - Name: pu.Name, - } - - ret = append(ret, puNoInfo) - } - - return ret, nil -} - -// Users returns a list of users in the specified M365 tenant -func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) { - ac, err := makeAC(ctx, acct, path.ExchangeService) - if err != nil { - return nil, clues.Stack(err).WithClues(ctx) - } - - us, err := ac.Users().GetAll(ctx, errs) - if err != nil { - return nil, err - } - - ret := make([]*User, 0, len(us)) - - for _, u := range us { - pu, err := parseUser(u) - if err != nil { - return nil, clues.Wrap(err, "formatting user data") - } - - userInfo, err := ac.Users().GetInfo(ctx, pu.ID) - if err != nil { - return nil, clues.Wrap(err, "getting user details") - } - - pu.Info = *userInfo - - ret = append(ret, pu) - } - - return ret, nil -} - -// parseUser extracts information from `models.Userable` we care about -func parseUser(item models.Userable) (*User, error) { - if item.GetUserPrincipalName() == nil { - return nil, clues.New("user missing principal name"). - With("user_id", ptr.Val(item.GetId())) - } - - u := &User{ - PrincipalName: ptr.Val(item.GetUserPrincipalName()), - ID: ptr.Val(item.GetId()), - Name: ptr.Val(item.GetDisplayName()), - } - - return u, nil -} - -// UserInfo returns the corso-specific set of user metadata. -func GetUserInfo( - ctx context.Context, - acct account.Account, - userID string, -) (*api.UserInfo, error) { - ac, err := makeAC(ctx, acct, path.ExchangeService) - if err != nil { - return nil, clues.Stack(err).WithClues(ctx) - } - - ui, err := ac.Users().GetInfo(ctx, userID) - if err != nil { - return nil, err - } - - return ui, nil -} - -// --------------------------------------------------------------------------- -// Sites -// --------------------------------------------------------------------------- - -// Site is the minimal information required to identify and display a SharePoint site. -type Site struct { - // WebURL is the url for the site, works as an alias for the user name. - WebURL string - - // ID is of the format: .. - // for example: contoso.sharepoint.com,abcdeab3-0ccc-4ce1-80ae-b32912c9468d,xyzud296-9f7c-44e1-af81-3c06d0d43007 - ID string - - // DisplayName is the human-readable name of the site. Normally the plaintext name that the - // user provided when they created the site, though it can be changed across time. - // Ex: webUrl: https://host.com/sites/TestingSite, displayName: "Testing Site" - DisplayName string -} - -// Sites returns a list of Sites in a specified M365 tenant -func Sites(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*Site, error) { - ac, err := makeAC(ctx, acct, path.SharePointService) - if err != nil { - return nil, clues.Stack(err).WithClues(ctx) - } - - return getAllSites(ctx, ac.Sites()) -} - -func getAllSites( - ctx context.Context, - ga getAller[models.Siteable], -) ([]*Site, error) { - sites, err := ga.GetAll(ctx, fault.New(true)) - if err != nil { - if clues.HasLabel(err, graph.LabelsNoSharePointLicense) { - return nil, clues.Stack(graph.ErrServiceNotEnabled, err) - } - - return nil, clues.Wrap(err, "retrieving sites") - } - - ret := make([]*Site, 0, len(sites)) - - for _, s := range sites { - ps, err := parseSite(s) - if err != nil { - return nil, clues.Wrap(err, "parsing siteable") - } - - ret = append(ret, ps) - } - - return ret, nil -} - -// parseSite extracts the information from `models.Siteable` we care about -func parseSite(item models.Siteable) (*Site, error) { - s := &Site{ - ID: ptr.Val(item.GetId()), - WebURL: ptr.Val(item.GetWebUrl()), - DisplayName: ptr.Val(item.GetDisplayName()), - } - - return s, nil -} - -// SitesMap retrieves all sites in the tenant, and returns two maps: one id-to-webURL, -// and one webURL-to-id. -func SitesMap( - ctx context.Context, - acct account.Account, - errs *fault.Bus, -) (idname.Cacher, error) { - sites, err := Sites(ctx, acct, errs) - if err != nil { - return idname.NewCache(nil), err - } - - itn := make(map[string]string, len(sites)) - - for _, s := range sites { - itn[s.ID] = s.WebURL - } - - return idname.NewCache(itn), nil -} - // --------------------------------------------------------------------------- // helpers // --------------------------------------------------------------------------- diff --git a/src/pkg/services/m365/sites.go b/src/pkg/services/m365/sites.go new file mode 100644 index 000000000..ab7b28bca --- /dev/null +++ b/src/pkg/services/m365/sites.go @@ -0,0 +1,99 @@ +package m365 + +import ( + "context" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + + "github.com/alcionai/corso/src/internal/common/idname" + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/path" +) + +// Site is the minimal information required to identify and display a SharePoint site. +type Site struct { + // WebURL is the url for the site, works as an alias for the user name. + WebURL string + + // ID is of the format: .. + // for example: contoso.sharepoint.com,abcdeab3-0ccc-4ce1-80ae-b32912c9468d,xyzud296-9f7c-44e1-af81-3c06d0d43007 + ID string + + // DisplayName is the human-readable name of the site. Normally the plaintext name that the + // user provided when they created the site, though it can be changed across time. + // Ex: webUrl: https://host.com/sites/TestingSite, displayName: "Testing Site" + DisplayName string +} + +// Sites returns a list of Sites in a specified M365 tenant +func Sites(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*Site, error) { + ac, err := makeAC(ctx, acct, path.SharePointService) + if err != nil { + return nil, clues.Stack(err).WithClues(ctx) + } + + return getAllSites(ctx, ac.Sites()) +} + +func getAllSites( + ctx context.Context, + ga getAller[models.Siteable], +) ([]*Site, error) { + sites, err := ga.GetAll(ctx, fault.New(true)) + if err != nil { + if clues.HasLabel(err, graph.LabelsNoSharePointLicense) { + return nil, clues.Stack(graph.ErrServiceNotEnabled, err) + } + + return nil, clues.Wrap(err, "retrieving sites") + } + + ret := make([]*Site, 0, len(sites)) + + for _, s := range sites { + ps, err := parseSite(s) + if err != nil { + return nil, clues.Wrap(err, "parsing siteable") + } + + ret = append(ret, ps) + } + + return ret, nil +} + +// parseSite extracts the information from `models.Siteable` we care about +func parseSite(item models.Siteable) (*Site, error) { + s := &Site{ + ID: ptr.Val(item.GetId()), + WebURL: ptr.Val(item.GetWebUrl()), + DisplayName: ptr.Val(item.GetDisplayName()), + } + + return s, nil +} + +// SitesMap retrieves all sites in the tenant, and returns two maps: one id-to-webURL, +// and one webURL-to-id. +func SitesMap( + ctx context.Context, + acct account.Account, + errs *fault.Bus, +) (idname.Cacher, error) { + sites, err := Sites(ctx, acct, errs) + if err != nil { + return idname.NewCache(nil), err + } + + itn := make(map[string]string, len(sites)) + + for _, s := range sites { + itn[s.ID] = s.WebURL + } + + return idname.NewCache(itn), nil +} diff --git a/src/pkg/services/m365/sites_test.go b/src/pkg/services/m365/sites_test.go new file mode 100644 index 000000000..a4d6a597d --- /dev/null +++ b/src/pkg/services/m365/sites_test.go @@ -0,0 +1,191 @@ +package m365 + +import ( + "context" + "testing" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + "github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/stretchr/testify/suite" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/internal/tester" + "github.com/alcionai/corso/src/internal/tester/tconfig" + "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/credentials" + "github.com/alcionai/corso/src/pkg/fault" +) + +type siteIntegrationSuite struct { + tester.Suite +} + +func TestSiteIntegrationSuite(t *testing.T) { + suite.Run(t, &siteIntegrationSuite{ + Suite: tester.NewIntegrationSuite( + t, + [][]string{tconfig.M365AcctCredEnvs}), + }) +} + +func (suite *siteIntegrationSuite) SetupSuite() { + ctx, flush := tester.NewContext(suite.T()) + defer flush() + + graph.InitializeConcurrencyLimiter(ctx, true, 4) +} + +func (suite *siteIntegrationSuite) TestSites() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + acct := tconfig.NewM365Account(t) + + sites, err := Sites(ctx, acct, fault.New(true)) + assert.NoError(t, err, clues.ToCore(err)) + assert.NotEmpty(t, sites) + + for _, s := range sites { + suite.Run("site_"+s.ID, func() { + t := suite.T() + assert.NotEmpty(t, s.WebURL) + assert.NotEmpty(t, s.ID) + assert.NotEmpty(t, s.DisplayName) + }) + } +} + +func (suite *siteIntegrationSuite) TestSites_InvalidCredentials() { + table := []struct { + name string + acct func(t *testing.T) account.Account + }{ + { + name: "Invalid Credentials", + acct: func(t *testing.T) account.Account { + a, err := account.NewAccount( + account.ProviderM365, + account.M365Config{ + M365: credentials.M365{ + AzureClientID: "Test", + AzureClientSecret: "without", + }, + AzureTenantID: "data", + }, + ) + require.NoError(t, err, clues.ToCore(err)) + + return a + }, + }, + { + name: "Empty Credentials", + acct: func(t *testing.T) account.Account { + // intentionally swallowing the error here + a, _ := account.NewAccount(account.ProviderM365) + return a + }, + }, + } + + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + sites, err := Sites(ctx, test.acct(t), fault.New(true)) + assert.Empty(t, sites, "returned some sites") + assert.NotNil(t, err) + }) + } +} + +// --------------------------------------------------------------------------- +// Unit +// --------------------------------------------------------------------------- + +type siteUnitSuite struct { + tester.Suite +} + +func TestSiteUnitSuite(t *testing.T) { + suite.Run(t, &siteUnitSuite{Suite: tester.NewUnitSuite(t)}) +} + +type mockGASites struct { + response []models.Siteable + err error +} + +func (m mockGASites) GetAll(context.Context, *fault.Bus) ([]models.Siteable, error) { + return m.response, m.err +} + +func (suite *siteUnitSuite) TestGetAllSites() { + table := []struct { + name string + mock func(context.Context) getAller[models.Siteable] + expectErr func(*testing.T, error) + }{ + { + name: "ok", + mock: func(ctx context.Context) getAller[models.Siteable] { + return mockGASites{[]models.Siteable{}, nil} + }, + expectErr: func(t *testing.T, err error) { + assert.NoError(t, err, clues.ToCore(err)) + }, + }, + { + name: "no sharepoint license", + mock: func(ctx context.Context) getAller[models.Siteable] { + odErr := odataerrors.NewODataError() + merr := odataerrors.NewMainError() + merr.SetCode(ptr.To("code")) + merr.SetMessage(ptr.To(string(graph.NoSPLicense))) + odErr.SetErrorEscaped(merr) + + return mockGASites{nil, graph.Stack(ctx, odErr)} + }, + expectErr: func(t *testing.T, err error) { + assert.ErrorIs(t, err, graph.ErrServiceNotEnabled, clues.ToCore(err)) + }, + }, + { + name: "arbitrary error", + mock: func(ctx context.Context) getAller[models.Siteable] { + odErr := odataerrors.NewODataError() + merr := odataerrors.NewMainError() + merr.SetCode(ptr.To("code")) + merr.SetMessage(ptr.To("message")) + odErr.SetErrorEscaped(merr) + + return mockGASites{nil, graph.Stack(ctx, odErr)} + }, + expectErr: func(t *testing.T, err error) { + assert.Error(t, err, clues.ToCore(err)) + }, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + gas := test.mock(ctx) + + _, err := getAllSites(ctx, gas) + test.expectErr(t, err) + }) + } +} diff --git a/src/pkg/services/m365/users.go b/src/pkg/services/m365/users.go new file mode 100644 index 000000000..35b3a0630 --- /dev/null +++ b/src/pkg/services/m365/users.go @@ -0,0 +1,211 @@ +package m365 + +import ( + "context" + + "github.com/alcionai/clues" + "github.com/microsoftgraph/msgraph-sdk-go/models" + + "github.com/alcionai/corso/src/internal/common/ptr" + "github.com/alcionai/corso/src/internal/m365/graph" + "github.com/alcionai/corso/src/pkg/account" + "github.com/alcionai/corso/src/pkg/fault" + "github.com/alcionai/corso/src/pkg/path" + "github.com/alcionai/corso/src/pkg/services/m365/api" +) + +// User is the minimal information required to identify and display a user. +type User struct { + PrincipalName string + ID string + Name string + Info api.UserInfo +} + +// UserNoInfo is the minimal information required to identify and display a user. +// TODO: Remove this once `UsersCompatNoInfo` is removed +type UserNoInfo struct { + PrincipalName string + ID string + Name string +} + +// UsersCompat returns a list of users in the specified M365 tenant. +// TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault +// package. +func UsersCompat(ctx context.Context, acct account.Account) ([]*User, error) { + errs := fault.New(true) + + us, err := Users(ctx, acct, errs) + if err != nil { + return nil, err + } + + return us, errs.Failure() +} + +// UsersCompatNoInfo returns a list of users in the specified M365 tenant. +// TODO: Remove this once `Info` is removed from the `User` struct and callers +// have switched over +func UsersCompatNoInfo(ctx context.Context, acct account.Account) ([]*UserNoInfo, error) { + errs := fault.New(true) + + us, err := usersNoInfo(ctx, acct, errs) + if err != nil { + return nil, err + } + + return us, errs.Failure() +} + +// UserHasMailbox returns true if the user has an exchange mailbox enabled +// false otherwise, and a nil pointer and an error in case of error +func UserHasMailbox(ctx context.Context, acct account.Account, userID string) (bool, error) { + ac, err := makeAC(ctx, acct, path.ExchangeService) + if err != nil { + return false, clues.Stack(err).WithClues(ctx) + } + + _, err = ac.Users().GetMailInbox(ctx, userID) + if err != nil { + if err := api.EvaluateMailboxError(err); err != nil { + return false, clues.Stack(err) + } + + return false, nil + } + + return true, nil +} + +// UserHasDrives returns true if the user has any drives +// false otherwise, and a nil pointer and an error in case of error +func UserHasDrives(ctx context.Context, acct account.Account, userID string) (bool, error) { + ac, err := makeAC(ctx, acct, path.OneDriveService) + if err != nil { + return false, clues.Stack(err).WithClues(ctx) + } + + return checkUserHasDrives(ctx, ac.Users(), userID) +} + +func checkUserHasDrives(ctx context.Context, dgdd getDefaultDriver, userID string) (bool, error) { + _, err := dgdd.GetDefaultDrive(ctx, userID) + if err != nil { + // we consider this a non-error case, since it + // answers the question the caller is asking. + if clues.HasLabel(err, graph.LabelsMysiteNotFound) || clues.HasLabel(err, graph.LabelsNoSharePointLicense) { + return false, nil + } + + if graph.IsErrUserNotFound(err) { + return false, clues.Stack(graph.ErrResourceOwnerNotFound, err) + } + + return false, clues.Stack(err) + } + + return true, nil +} + +// usersNoInfo returns a list of users in the specified M365 tenant - with no info +// TODO: Remove this once we remove `Info` from `Users` and instead rely on the `GetUserInfo` API +// to get user information +func usersNoInfo(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*UserNoInfo, error) { + ac, err := makeAC(ctx, acct, path.UnknownService) + if err != nil { + return nil, clues.Stack(err).WithClues(ctx) + } + + us, err := ac.Users().GetAll(ctx, errs) + if err != nil { + return nil, err + } + + ret := make([]*UserNoInfo, 0, len(us)) + + for _, u := range us { + pu, err := parseUser(u) + if err != nil { + return nil, clues.Wrap(err, "formatting user data") + } + + puNoInfo := &UserNoInfo{ + PrincipalName: pu.PrincipalName, + ID: pu.ID, + Name: pu.Name, + } + + ret = append(ret, puNoInfo) + } + + return ret, nil +} + +// Users returns a list of users in the specified M365 tenant +func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) { + ac, err := makeAC(ctx, acct, path.ExchangeService) + if err != nil { + return nil, clues.Stack(err).WithClues(ctx) + } + + us, err := ac.Users().GetAll(ctx, errs) + if err != nil { + return nil, err + } + + ret := make([]*User, 0, len(us)) + + for _, u := range us { + pu, err := parseUser(u) + if err != nil { + return nil, clues.Wrap(err, "formatting user data") + } + + userInfo, err := ac.Users().GetInfo(ctx, pu.ID) + if err != nil { + return nil, clues.Wrap(err, "getting user details") + } + + pu.Info = *userInfo + + ret = append(ret, pu) + } + + return ret, nil +} + +// parseUser extracts information from `models.Userable` we care about +func parseUser(item models.Userable) (*User, error) { + if item.GetUserPrincipalName() == nil { + return nil, clues.New("user missing principal name"). + With("user_id", ptr.Val(item.GetId())) + } + + u := &User{ + PrincipalName: ptr.Val(item.GetUserPrincipalName()), + ID: ptr.Val(item.GetId()), + Name: ptr.Val(item.GetDisplayName()), + } + + return u, nil +} + +// UserInfo returns the corso-specific set of user metadata. +func GetUserInfo( + ctx context.Context, + acct account.Account, + userID string, +) (*api.UserInfo, error) { + ac, err := makeAC(ctx, acct, path.ExchangeService) + if err != nil { + return nil, clues.Stack(err).WithClues(ctx) + } + + ui, err := ac.Users().GetInfo(ctx, userID) + if err != nil { + return nil, err + } + + return ui, nil +} diff --git a/src/pkg/services/m365/m365_test.go b/src/pkg/services/m365/users_test.go similarity index 64% rename from src/pkg/services/m365/m365_test.go rename to src/pkg/services/m365/users_test.go index 0124f13f2..78a27e111 100644 --- a/src/pkg/services/m365/m365_test.go +++ b/src/pkg/services/m365/users_test.go @@ -23,26 +23,29 @@ import ( "github.com/alcionai/corso/src/pkg/services/m365/api" ) -type M365IntegrationSuite struct { +type userIntegrationSuite struct { tester.Suite + acct account.Account } -func TestM365IntegrationSuite(t *testing.T) { - suite.Run(t, &M365IntegrationSuite{ +func TestUserIntegrationSuite(t *testing.T) { + suite.Run(t, &userIntegrationSuite{ Suite: tester.NewIntegrationSuite( t, [][]string{tconfig.M365AcctCredEnvs}), }) } -func (suite *M365IntegrationSuite) SetupSuite() { +func (suite *userIntegrationSuite) SetupSuite() { ctx, flush := tester.NewContext(suite.T()) defer flush() graph.InitializeConcurrencyLimiter(ctx, true, 4) + + suite.acct = tconfig.NewM365Account(suite.T()) } -func (suite *M365IntegrationSuite) TestUsers() { +func (suite *userIntegrationSuite) TestUsers() { t := suite.T() ctx, flush := tester.NewContext(t) @@ -50,9 +53,7 @@ func (suite *M365IntegrationSuite) TestUsers() { graph.InitializeConcurrencyLimiter(ctx, true, 4) - acct := tconfig.NewM365Account(suite.T()) - - users, err := Users(ctx, acct, fault.New(true)) + users, err := Users(ctx, suite.acct, fault.New(true)) assert.NoError(t, err, clues.ToCore(err)) assert.NotEmpty(t, users) @@ -68,7 +69,7 @@ func (suite *M365IntegrationSuite) TestUsers() { } } -func (suite *M365IntegrationSuite) TestUsersCompat_HasNoInfo() { +func (suite *userIntegrationSuite) TestUsersCompat_HasNoInfo() { t := suite.T() ctx, flush := tester.NewContext(t) @@ -91,7 +92,7 @@ func (suite *M365IntegrationSuite) TestUsersCompat_HasNoInfo() { } } -func (suite *M365IntegrationSuite) TestUserHasMailbox() { +func (suite *userIntegrationSuite) TestUserHasMailbox() { t := suite.T() ctx, flush := tester.NewContext(t) @@ -107,7 +108,7 @@ func (suite *M365IntegrationSuite) TestUserHasMailbox() { assert.True(t, enabled) } -func (suite *M365IntegrationSuite) TestUserHasDrive() { +func (suite *userIntegrationSuite) TestUserHasDrive() { t := suite.T() ctx, flush := tester.NewContext(t) @@ -123,34 +124,155 @@ func (suite *M365IntegrationSuite) TestUserHasDrive() { assert.True(t, enabled) } -func (suite *M365IntegrationSuite) TestSites() { - t := suite.T() +func (suite *userIntegrationSuite) TestUsers_InvalidCredentials() { + table := []struct { + name string + acct func(t *testing.T) account.Account + }{ + { + name: "Invalid Credentials", + acct: func(t *testing.T) account.Account { + a, err := account.NewAccount( + account.ProviderM365, + account.M365Config{ + M365: credentials.M365{ + AzureClientID: "Test", + AzureClientSecret: "without", + }, + AzureTenantID: "data", + }, + ) + require.NoError(t, err, clues.ToCore(err)) - ctx, flush := tester.NewContext(t) - defer flush() + return a + }, + }, + } - acct := tconfig.NewM365Account(t) - - sites, err := Sites(ctx, acct, fault.New(true)) - assert.NoError(t, err, clues.ToCore(err)) - assert.NotEmpty(t, sites) - - for _, s := range sites { - suite.Run("site_"+s.ID, func() { + for _, test := range table { + suite.Run(test.name, func() { t := suite.T() - assert.NotEmpty(t, s.WebURL) - assert.NotEmpty(t, s.ID) - assert.NotEmpty(t, s.DisplayName) + + ctx, flush := tester.NewContext(t) + defer flush() + + users, err := Users(ctx, test.acct(t), fault.New(true)) + assert.Empty(t, users, "returned some users") + assert.NotNil(t, err) }) } } -type m365UnitSuite struct { +func (suite *userIntegrationSuite) TestGetUserInfo() { + table := []struct { + name string + user string + expect *api.UserInfo + expectErr require.ErrorAssertionFunc + }{ + { + name: "standard test user", + user: tconfig.M365UserID(suite.T()), + expect: &api.UserInfo{ + ServicesEnabled: map[path.ServiceType]struct{}{ + path.ExchangeService: {}, + path.OneDriveService: {}, + }, + Mailbox: api.MailboxInfo{ + Purpose: "user", + ErrGetMailBoxSetting: nil, + }, + }, + expectErr: require.NoError, + }, + { + name: "user does not exist", + user: uuid.NewString(), + expect: &api.UserInfo{ + ServicesEnabled: map[path.ServiceType]struct{}{}, + Mailbox: api.MailboxInfo{}, + }, + expectErr: require.Error, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + result, err := GetUserInfo(ctx, suite.acct, test.user) + test.expectErr(t, err, clues.ToCore(err)) + + if err != nil { + return + } + + assert.Equal(t, test.expect.ServicesEnabled, result.ServicesEnabled) + }) + } +} + +func (suite *userIntegrationSuite) TestGetUserInfo_userWithoutDrive() { + userID := tconfig.M365UserID(suite.T()) + + table := []struct { + name string + user string + expect *api.UserInfo + }{ + { + name: "user without drive and exchange", + user: "a53c26f7-5100-4acb-a910-4d20960b2c19", // User: testevents@10rqc2.onmicrosoft.com + expect: &api.UserInfo{ + ServicesEnabled: map[path.ServiceType]struct{}{}, + Mailbox: api.MailboxInfo{ + ErrGetMailBoxSetting: []error{api.ErrMailBoxSettingsNotFound}, + }, + }, + }, + { + name: "user with drive and exchange", + user: userID, + expect: &api.UserInfo{ + ServicesEnabled: map[path.ServiceType]struct{}{ + path.ExchangeService: {}, + path.OneDriveService: {}, + }, + Mailbox: api.MailboxInfo{ + Purpose: "user", + ErrGetMailBoxSetting: []error{}, + }, + }, + }, + } + for _, test := range table { + suite.Run(test.name, func() { + t := suite.T() + + ctx, flush := tester.NewContext(t) + defer flush() + + result, err := GetUserInfo(ctx, suite.acct, test.user) + require.NoError(t, err, clues.ToCore(err)) + assert.Equal(t, test.expect.ServicesEnabled, result.ServicesEnabled) + assert.Equal(t, test.expect.Mailbox.ErrGetMailBoxSetting, result.Mailbox.ErrGetMailBoxSetting) + assert.Equal(t, test.expect.Mailbox.Purpose, result.Mailbox.Purpose) + }) + } +} + +// --------------------------------------------------------------------------- +// Unit +// --------------------------------------------------------------------------- + +type userUnitSuite struct { tester.Suite } -func TestM365UnitSuite(t *testing.T) { - suite.Run(t, &m365UnitSuite{Suite: tester.NewUnitSuite(t)}) +func TestUserUnitSuite(t *testing.T) { + suite.Run(t, &userUnitSuite{Suite: tester.NewUnitSuite(t)}) } type mockDGDD struct { @@ -162,7 +284,7 @@ func (m mockDGDD) GetDefaultDrive(context.Context, string) (models.Driveable, er return m.response, m.err } -func (suite *m365UnitSuite) TestCheckUserHasDrives() { +func (suite *userUnitSuite) TestCheckUserHasDrives() { table := []struct { name string mock func(context.Context) getDefaultDriver @@ -275,300 +397,3 @@ func (suite *m365UnitSuite) TestCheckUserHasDrives() { }) } } - -type mockGASites struct { - response []models.Siteable - err error -} - -func (m mockGASites) GetAll(context.Context, *fault.Bus) ([]models.Siteable, error) { - return m.response, m.err -} - -func (suite *m365UnitSuite) TestGetAllSites() { - table := []struct { - name string - mock func(context.Context) getAller[models.Siteable] - expectErr func(*testing.T, error) - }{ - { - name: "ok", - mock: func(ctx context.Context) getAller[models.Siteable] { - return mockGASites{[]models.Siteable{}, nil} - }, - expectErr: func(t *testing.T, err error) { - assert.NoError(t, err, clues.ToCore(err)) - }, - }, - { - name: "no sharepoint license", - mock: func(ctx context.Context) getAller[models.Siteable] { - odErr := odataerrors.NewODataError() - merr := odataerrors.NewMainError() - merr.SetCode(ptr.To("code")) - merr.SetMessage(ptr.To(string(graph.NoSPLicense))) - odErr.SetErrorEscaped(merr) - - return mockGASites{nil, graph.Stack(ctx, odErr)} - }, - expectErr: func(t *testing.T, err error) { - assert.ErrorIs(t, err, graph.ErrServiceNotEnabled, clues.ToCore(err)) - }, - }, - { - name: "arbitrary error", - mock: func(ctx context.Context) getAller[models.Siteable] { - odErr := odataerrors.NewODataError() - merr := odataerrors.NewMainError() - merr.SetCode(ptr.To("code")) - merr.SetMessage(ptr.To("message")) - odErr.SetErrorEscaped(merr) - - return mockGASites{nil, graph.Stack(ctx, odErr)} - }, - expectErr: func(t *testing.T, err error) { - assert.Error(t, err, clues.ToCore(err)) - }, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - gas := test.mock(ctx) - - _, err := getAllSites(ctx, gas) - test.expectErr(t, err) - }) - } -} - -type DiscoveryIntgSuite struct { - tester.Suite - acct account.Account -} - -func TestDiscoveryIntgSuite(t *testing.T) { - suite.Run(t, &DiscoveryIntgSuite{ - Suite: tester.NewIntegrationSuite( - t, - [][]string{tconfig.M365AcctCredEnvs}), - }) -} - -func (suite *DiscoveryIntgSuite) SetupSuite() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - graph.InitializeConcurrencyLimiter(ctx, true, 4) - - suite.acct = tconfig.NewM365Account(t) -} - -func (suite *DiscoveryIntgSuite) TestUsers() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - errs := fault.New(true) - - users, err := Users(ctx, suite.acct, errs) - assert.NoError(t, err, clues.ToCore(err)) - - ferrs := errs.Errors() - assert.Nil(t, ferrs.Failure) - assert.Empty(t, ferrs.Recovered) - assert.NotEmpty(t, users) -} - -func (suite *DiscoveryIntgSuite) TestUsers_InvalidCredentials() { - table := []struct { - name string - acct func(t *testing.T) account.Account - }{ - { - name: "Invalid Credentials", - acct: func(t *testing.T) account.Account { - a, err := account.NewAccount( - account.ProviderM365, - account.M365Config{ - M365: credentials.M365{ - AzureClientID: "Test", - AzureClientSecret: "without", - }, - AzureTenantID: "data", - }, - ) - require.NoError(t, err, clues.ToCore(err)) - - return a - }, - }, - } - - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - users, err := Users(ctx, test.acct(t), fault.New(true)) - assert.Empty(t, users, "returned some users") - assert.NotNil(t, err) - }) - } -} - -func (suite *DiscoveryIntgSuite) TestSites_InvalidCredentials() { - table := []struct { - name string - acct func(t *testing.T) account.Account - }{ - { - name: "Invalid Credentials", - acct: func(t *testing.T) account.Account { - a, err := account.NewAccount( - account.ProviderM365, - account.M365Config{ - M365: credentials.M365{ - AzureClientID: "Test", - AzureClientSecret: "without", - }, - AzureTenantID: "data", - }, - ) - require.NoError(t, err, clues.ToCore(err)) - - return a - }, - }, - { - name: "Empty Credentials", - acct: func(t *testing.T) account.Account { - // intentionally swallowing the error here - a, _ := account.NewAccount(account.ProviderM365) - return a - }, - }, - } - - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - sites, err := Sites(ctx, test.acct(t), fault.New(true)) - assert.Empty(t, sites, "returned some sites") - assert.NotNil(t, err) - }) - } -} - -func (suite *DiscoveryIntgSuite) TestGetUserInfo() { - table := []struct { - name string - user string - expect *api.UserInfo - expectErr require.ErrorAssertionFunc - }{ - { - name: "standard test user", - user: tconfig.M365UserID(suite.T()), - expect: &api.UserInfo{ - ServicesEnabled: map[path.ServiceType]struct{}{ - path.ExchangeService: {}, - path.OneDriveService: {}, - }, - Mailbox: api.MailboxInfo{ - Purpose: "user", - ErrGetMailBoxSetting: nil, - }, - }, - expectErr: require.NoError, - }, - { - name: "user does not exist", - user: uuid.NewString(), - expect: &api.UserInfo{ - ServicesEnabled: map[path.ServiceType]struct{}{}, - Mailbox: api.MailboxInfo{}, - }, - expectErr: require.Error, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - result, err := GetUserInfo(ctx, suite.acct, test.user) - test.expectErr(t, err, clues.ToCore(err)) - - if err != nil { - return - } - - assert.Equal(t, test.expect.ServicesEnabled, result.ServicesEnabled) - }) - } -} - -func (suite *DiscoveryIntgSuite) TestGetUserInfo_userWithoutDrive() { - userID := tconfig.M365UserID(suite.T()) - - table := []struct { - name string - user string - expect *api.UserInfo - }{ - { - name: "user without drive and exchange", - user: "a53c26f7-5100-4acb-a910-4d20960b2c19", // User: testevents@10rqc2.onmicrosoft.com - expect: &api.UserInfo{ - ServicesEnabled: map[path.ServiceType]struct{}{}, - Mailbox: api.MailboxInfo{ - ErrGetMailBoxSetting: []error{api.ErrMailBoxSettingsNotFound}, - }, - }, - }, - { - name: "user with drive and exchange", - user: userID, - expect: &api.UserInfo{ - ServicesEnabled: map[path.ServiceType]struct{}{ - path.ExchangeService: {}, - path.OneDriveService: {}, - }, - Mailbox: api.MailboxInfo{ - Purpose: "user", - ErrGetMailBoxSetting: []error{}, - }, - }, - }, - } - for _, test := range table { - suite.Run(test.name, func() { - t := suite.T() - - ctx, flush := tester.NewContext(t) - defer flush() - - result, err := GetUserInfo(ctx, suite.acct, test.user) - require.NoError(t, err, clues.ToCore(err)) - assert.Equal(t, test.expect.ServicesEnabled, result.ServicesEnabled) - assert.Equal(t, test.expect.Mailbox.ErrGetMailBoxSetting, result.Mailbox.ErrGetMailBoxSetting) - assert.Equal(t, test.expect.Mailbox.Purpose, result.Mailbox.Purpose) - }) - } -}