From be7b77876998cffdf55cdcaa66902295bddc5f69 Mon Sep 17 00:00:00 2001 From: Danny Date: Mon, 15 Aug 2022 13:50:06 -0400 Subject: [PATCH] GC Contacts requires additional tests placed in Exchange Services (#475) Test coverage extended to GraphQuery and GraphIterateFuncs for exchange.Mail and exchange.Contact use cases. --- .../exchange/exchange_service_test.go | 230 ++++++++++++++++-- .../connector/exchange/service_functions.go | 29 +-- .../connector/exchange/service_query.go | 107 +++++--- 3 files changed, 299 insertions(+), 67 deletions(-) diff --git a/src/internal/connector/exchange/exchange_service_test.go b/src/internal/connector/exchange/exchange_service_test.go index 467e4f855..43d21457f 100644 --- a/src/internal/connector/exchange/exchange_service_test.go +++ b/src/internal/connector/exchange/exchange_service_test.go @@ -3,43 +3,102 @@ package exchange import ( "testing" + absser "github.com/microsoft/kiota-abstractions-go/serialization" + msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core" + "github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/stretchr/testify/suite" "github.com/alcionai/corso/internal/tester" + "github.com/alcionai/corso/pkg/account" "github.com/alcionai/corso/pkg/selectors" ) type ExchangeServiceSuite struct { suite.Suite + es *exchangeService } func TestExchangeServiceSuite(t *testing.T) { + if err := tester.RunOnAny( + tester.CorsoCITests, + tester.CorsoGraphConnectorTests, + ); err != nil { + t.Skip(err) + } suite.Run(t, new(ExchangeServiceSuite)) } -// TestExchangeService_optionsForMessages checks to ensure approved query +func (suite *ExchangeServiceSuite) SetupSuite() { + t := suite.T() + _, err := tester.GetRequiredEnvVars(tester.M365AcctCredEnvs...) + require.NoError(t, err) + + a := tester.NewM365Account(t) + require.NoError(t, err) + m365, err := a.M365Config() + require.NoError(t, err) + service, err := createService(m365, false) + require.NoError(t, err) + suite.es = service +} + +// TestCreateService verifies that services are created +// when called with the correct range of params. NOTE: +// incorrect tenant or password information will NOT generate +// an error. +func (suite *ExchangeServiceSuite) TestCreateService() { + creds := suite.es.credentials + invalidCredentials := suite.es.credentials + invalidCredentials.ClientSecret = "" + + tests := []struct { + name string + credentials account.M365Config + checkErr assert.ErrorAssertionFunc + }{ + { + name: "Valid Service Creation", + credentials: creds, + checkErr: assert.NoError, + }, + { + name: "Invalid Service Creation", + credentials: invalidCredentials, + checkErr: assert.Error, + }, + } + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + t.Log(test.credentials.ClientSecret) + _, err := createService(test.credentials, false) + test.checkErr(t, err) + }) + } +} + +// TestOptionsForMessages checks to ensure approved query // options are added to the type specific RequestBuildConfiguration. Expected // will be +1 on all select parameters -func (suite *ExchangeServiceSuite) TestExchangeService_optionsForMessages() { +func (suite *ExchangeServiceSuite) TestOptionsForMessages() { tests := []struct { name string params []string checkError assert.ErrorAssertionFunc }{ { - name: "Accepted", + name: "Valid Message Option", params: []string{"subject"}, checkError: assert.NoError, }, { - name: "Multiple Accepted", + name: "Multiple Message Options: Accepted", params: []string{"webLink", "parentFolderId"}, checkError: assert.NoError, }, { - name: "Incorrect param", + name: "Invalid Message Parameter", params: []string{"status"}, checkError: assert.Error, }, @@ -55,10 +114,10 @@ func (suite *ExchangeServiceSuite) TestExchangeService_optionsForMessages() { } } -// TestExchangeService_optionsForFolders ensures that approved query options +// TestOptionsForFolders ensures that approved query options // are added to the RequestBuildConfiguration. Expected will always be +1 // on than the input as "id" are always included within the select parameters -func (suite *ExchangeServiceSuite) TestExchangeService_optionsForFolders() { +func (suite *ExchangeServiceSuite) TestOptionsForFolders() { tests := []struct { name string params []string @@ -66,19 +125,19 @@ func (suite *ExchangeServiceSuite) TestExchangeService_optionsForFolders() { expected int }{ { - name: "Accepted", - params: []string{"displayName"}, + name: "Valid Folder Option", + params: []string{"parentFolderId"}, checkError: assert.NoError, expected: 2, }, { - name: "Multiple Accepted", - params: []string{"displayName", "parentFolderId"}, + name: "Multiple Folder Options: Valid", + params: []string{"displayName", "isHidden"}, checkError: assert.NoError, expected: 3, }, { - name: "Incorrect param", + name: "Invalid Folder option param", params: []string{"status"}, checkError: assert.Error, }, @@ -94,8 +153,47 @@ func (suite *ExchangeServiceSuite) TestExchangeService_optionsForFolders() { } } -// NOTE the requirements are in PR 475 -func (suite *ExchangeServiceSuite) TestExchangeService_SetupExchangeCollection() { +// TestOptionsForContacts similar to TestExchangeService_optionsForFolders +func (suite *ExchangeServiceSuite) TestOptionsForContacts() { + tests := []struct { + name string + params []string + checkError assert.ErrorAssertionFunc + expected int + }{ + { + name: "Valid Contact Option", + params: []string{"displayName"}, + checkError: assert.NoError, + expected: 2, + }, + { + name: "Multiple Contact Options: Valid", + params: []string{"displayName", "parentFolderId"}, + checkError: assert.NoError, + expected: 3, + }, + { + name: "Invalid Contact Option param", + params: []string{"status"}, + checkError: assert.Error, + }, + } + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + options, err := optionsForContacts(test.params) + test.checkError(t, err) + if err == nil { + suite.Equal(test.expected, len(options.QueryParameters.Select)) + } + }) + } +} + +// TestSetupExchangeCollection ensures that the helper +// function SetupExchangeCollectionVars returns a non-nil variable for returns +// in regards to the selector.ExchangeScope. +func (suite *ExchangeServiceSuite) TestSetupExchangeCollection() { userID := tester.M365UserID(suite.T()) sel := selectors.NewExchangeBackup() sel.Include(sel.Users([]string{userID})) @@ -116,3 +214,107 @@ func (suite *ExchangeServiceSuite) TestExchangeService_SetupExchangeCollection() }) } } + +// TestGraphQueryFunctions verifies if Query functions APIs +// through Microsoft Graph are functional +func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() { + userID := tester.M365UserID(suite.T()) + tests := []struct { + name string + function GraphQuery + }{ + { + name: "GraphQuery: Get All Messages For User", + function: GetAllMessagesForUser, + }, + { + name: "GraphQuery: Get All Contacts For User", + function: GetAllContactsForUser, + }, + { + name: "GraphQuery: Get All Folders", + function: GetAllFolderNamesForUser, + }, + } + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + response, err := test.function(suite.es, userID) + assert.NoError(t, err) + assert.NotNil(t, response) + }) + } +} + +// TestIterativeFunctions verifies that GraphQuery to Iterate +// functions are valid for current versioning of msgraph-go-sdk +func (suite *ExchangeServiceSuite) TestIterativeFunctions() { + userID := tester.M365UserID(suite.T()) + sel := selectors.NewExchangeBackup() + sel.Include(sel.Users([]string{userID})) + eb, err := sel.ToExchangeBackup() + require.NoError(suite.T(), err) + scopes := eb.Scopes() + var mailScope, contactScope selectors.ExchangeScope + for _, scope := range scopes { + if scope.IncludesCategory(selectors.ExchangeContactFolder) { + contactScope = scope + } + if scope.IncludesCategory(selectors.ExchangeMail) { + mailScope = scope + } + } + + tests := []struct { + name string + queryFunction GraphQuery + iterativeFunction GraphIterateFunc + scope selectors.ExchangeScope + transformer absser.ParsableFactory + }{ + { + name: "Mail Iterative Check", + queryFunction: GetAllMessagesForUser, + iterativeFunction: IterateSelectAllMessagesForCollections, + scope: mailScope, + transformer: models.CreateMessageCollectionResponseFromDiscriminatorValue, + }, { + name: "Contacts Iterative Check", + queryFunction: GetAllContactsForUser, + iterativeFunction: IterateAllContactsForCollection, + scope: contactScope, + transformer: models.CreateContactFromDiscriminatorValue, + }, { + name: "Folder Iterative Check", + queryFunction: GetAllFolderNamesForUser, + iterativeFunction: IterateFilterFolderDirectoriesForCollections, + scope: mailScope, + transformer: models.CreateMailFolderCollectionResponseFromDiscriminatorValue, + }, + } + for _, test := range tests { + suite.T().Run(test.name, func(t *testing.T) { + response, err := test.queryFunction(suite.es, userID) + require.NoError(t, err) + // Create Iterator + pageIterator, err := msgraphgocore.NewPageIterator(response, + &suite.es.adapter, + test.transformer) + require.NoError(t, err) + // Create collection for iterate test + collections := make(map[string]*Collection) + var errs error + // callbackFunc iterates through all models.Messageable and fills exchange.Collection.jobs[] + // with corresponding item IDs. New collections are created for each directory + callbackFunc := test.iterativeFunction( + "testingTenant", + test.scope, + errs, false, + suite.es.credentials, + collections, + nil) + + iterateError := pageIterator.Iterate(callbackFunc) + require.NoError(t, iterateError) + }) + } +} diff --git a/src/internal/connector/exchange/service_functions.go b/src/internal/connector/exchange/service_functions.go index c0728ce2a..a7fe18a5d 100644 --- a/src/internal/connector/exchange/service_functions.go +++ b/src/internal/connector/exchange/service_functions.go @@ -29,6 +29,9 @@ type exchangeService struct { credentials account.M365Config } +///------------------------------------------------------------ +// Functions to comply with graph.Service Interface +//------------------------------------------------------- func (es *exchangeService) Client() *msgraphsdk.GraphServiceClient { return &es.client } @@ -41,6 +44,9 @@ func (es *exchangeService) ErrPolicy() bool { return es.failFast } +// createService internal constructor for exchangeService struct returns an error +// iff the params for the entry are incorrect (e.g. len(TenantID) == 0, etc.) +// NOTE: Incorrect account information will result in errors on subsequent queries. func createService(credentials account.M365Config, shouldFailFast bool) (*exchangeService, error) { adapter, err := graph.CreateAdapter( credentials.TenantID, @@ -89,13 +95,7 @@ func GetAllMailFolders(gs graph.Service, user, nameContains string) ([]MailFolde mfs = []MailFolder{} err error ) - - opts, err := optionsForMailFolders([]string{"id", "displayName"}) - if err != nil { - return nil, err - } - - resp, err := gs.Client().UsersById(user).MailFolders().GetWithRequestConfigurationAndResponseHandler(opts, nil) + resp, err := GetAllFolderNamesForUser(gs, user) if err != nil { return nil, err } @@ -137,21 +137,12 @@ func GetAllMailFolders(gs graph.Service, user, nameContains string) ([]MailFolde func GetMailFolderID(service graph.Service, folderName, user string) (*string, error) { var errs error var folderID *string - options, err := optionsForMailFolders([]string{"displayName"}) + + response, err := GetAllFolderNamesForUser(service, user) if err != nil { return nil, err } - response, err := service. - Client(). - UsersById(user). - MailFolders(). - GetWithRequestConfigurationAndResponseHandler(options, nil) - if err != nil { - return nil, err - } - if response == nil { - return nil, errors.New("mail folder query to m365 back store returned nil") - } + pageIterator, err := msgraphgocore.NewPageIterator( response, service.Adapter(), diff --git a/src/internal/connector/exchange/service_query.go b/src/internal/connector/exchange/service_query.go index 5faeef5b9..29a37d2e6 100644 --- a/src/internal/connector/exchange/service_query.go +++ b/src/internal/connector/exchange/service_query.go @@ -64,13 +64,13 @@ func GetAllContactsForUser(gs graph.Service, user string) (absser.Parsable, erro // GetAllFolderDisplayNamesForUser is a GraphQuery function for getting FolderId and display // names for Mail Folder. All other information for the MailFolder object is omitted. -func GetAllFolderNamesForUser(gs graph.Service, identities []string) (absser.Parsable, error) { +func GetAllFolderNamesForUser(gs graph.Service, user string) (absser.Parsable, error) { options, err := optionsForMailFolders([]string{"id", "displayName"}) if err != nil { return nil, err } - return gs.Client().UsersById(identities[0]).MailFolders().GetWithRequestConfigurationAndResponseHandler(options, nil) + return gs.Client().UsersById(user).MailFolders().GetWithRequestConfigurationAndResponseHandler(options, nil) } // GraphIterateFuncs are iterate functions to be used with the M365 iterators (e.g. msgraphgocore.NewPageIterator) @@ -216,6 +216,60 @@ func IterateAndFilterMessagesForCollections( } } +func IterateFilterFolderDirectoriesForCollections( + tenant string, + scope selectors.ExchangeScope, + errs error, + failFast bool, + credentials account.M365Config, + collections map[string]*Collection, + statusCh chan<- *support.ConnectorOperationStatus, +) func(any) bool { + var ( + service graph.Service + err error + ) + return func(folderItem any) bool { + user := scope.Get(selectors.ExchangeUser)[0] + folder, ok := folderItem.(models.MailFolderable) + if !ok { + errs = support.WrapAndAppend( + user, + errors.New("unable to transform folderable item"), + errs, + ) + + return true + } + if !scope.Contains(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { + return true + } + directory := *folder.GetId() + service, err = createService(credentials, failFast) + if err != nil { + errs = support.WrapAndAppend( + *folder.GetDisplayName(), + errors.Wrap( + err, + "unable to create service a folder query service for "+user, + ), + errs, + ) + return true + } + temp := NewCollection( + user, + []string{tenant, user, mailCategory, directory}, + messages, + service, + statusCh, + ) + collections[directory] = &temp + + return true + } +} + func CollectMailFolders( scope selectors.ExchangeScope, tenant string, @@ -230,7 +284,7 @@ func CollectMailFolders( return errors.New("unable to create a mail folder query service for " + user) } - query, err := GetAllFolderNamesForUser(queryService, []string{user}) + query, err := GetAllFolderNamesForUser(queryService, user) if err != nil { return fmt.Errorf( "unable to query mail folder for %s: details: %s", @@ -247,37 +301,16 @@ func CollectMailFolders( if err != nil { return errors.Wrap(err, "unable to create iterator during mail folder query service") } - var service graph.Service - callbackFunc := func(pageItem any) bool { - folder, ok := pageItem.(models.MailFolderable) - if !ok { - err = support.WrapAndAppend(user, errors.New("unable to transform folderable item"), err) - return true - } - if !scope.Contains(selectors.ExchangeMailFolder, *folder.GetDisplayName()) { - return true - } - directory := *folder.GetId() - service, err = createService(credentials, failFast) - if err != nil { - err = support.WrapAndAppend( - *folder.GetDisplayName(), - errors.New("unable to create service a folder query service for "+user), - err, - ) - return true - } - temp := NewCollection( - user, - []string{tenant, user, mailCategory, directory}, - messages, - service, - statusCh, - ) - collections[directory] = &temp - return true - } + callbackFunc := IterateFilterFolderDirectoriesForCollections( + tenant, + scope, + err, + failFast, + credentials, + collections, + statusCh, + ) iterateFailure := pageIterator.Iterate(callbackFunc) if iterateFailure != nil { @@ -393,7 +426,13 @@ func buildOptions(options []string, optID optionIdentifier) ([]string, error) { fieldsForContacts := map[string]int{ "id": 1, - "parentFolderId": 2, + "companyName": 2, + "department": 3, + "displayName": 4, + "fileAs": 5, + "givenName": 6, + "manager": 7, + "parentFolderId": 8, } returnedOptions := []string{"id"}