first pass migrate graph api to subfolder (#2003)

## Description

In order to establish a standard api around our
graph client usage, and thus be able to mock
for testing, we need to migrate graph client
usage into another pacakge.  This is the first
step in that process of refactoring.

All changes are code relocation importing and
exporting may change as needed.  No logic
was altered.

## Does this PR need a docs update or release note?

- [x]  No 

## Type of change

- [x] 🤖 Test
- [x] 🐹 Trivial/Minor

## Issue(s)

* #1967

## Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-01-03 18:35:32 -07:00 committed by GitHub
parent 753ede5a1a
commit e5edbfd77c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 1301 additions and 1178 deletions

View File

@ -19,6 +19,7 @@ import (
"github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector" "github.com/alcionai/corso/src/internal/connector"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -94,7 +95,7 @@ func runDisplayM365JSON(
gs graph.Servicer, gs graph.Servicer,
) error { ) error {
var ( var (
get exchange.GraphRetrievalFunc get api.GraphRetrievalFunc
serializeFunc exchange.GraphSerializeFunc serializeFunc exchange.GraphSerializeFunc
cat = graph.StringToPathCategory(category) cat = graph.StringToPathCategory(category)
) )

View File

@ -0,0 +1,50 @@
package api
import (
"context"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/alcionai/corso/src/internal/connector/graph"
)
// ---------------------------------------------------------------------------
// common types
// ---------------------------------------------------------------------------
// DeltaUpdate holds the results of a current delta token. It normally
// gets produced when aggregating the addition and removal of items in
// a delta-queriable folder.
type DeltaUpdate struct {
// the deltaLink itself
URL string
// true if the old delta was marked as invalid
Reset bool
}
// GraphQuery represents functions which perform exchange-specific queries
// into M365 backstore. Responses -> returned items will only contain the information
// that is included in the options
// TODO: use selector or path for granularity into specific folders or specific date ranges
type GraphQuery func(ctx context.Context, gs graph.Servicer, userID string) (serialization.Parsable, error)
// GraphRetrievalFunctions are functions from the Microsoft Graph API that retrieve
// the default associated data of a M365 object. This varies by object. Additional
// Queries must be run to obtain the omitted fields.
type GraphRetrievalFunc func(
ctx context.Context,
gs graph.Servicer,
user, m365ID string,
) (serialization.Parsable, error)
// ---------------------------------------------------------------------------
// interfaces
// ---------------------------------------------------------------------------
// API is a struct used to fulfill the interface for exchange
// queries that are traditionally backed by GraphAPI. A
// struct is used in this case, instead of deferring to
// pure function wrappers, so that the boundary separates the
// granular implementation of the graphAPI and kiota away
// from the exchange package's broader intents.
// type API struct{}

View File

@ -0,0 +1,189 @@
package api
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/account"
)
type ExchangeServiceSuite struct {
suite.Suite
gs graph.Servicer
credentials account.M365Config
}
func TestExchangeServiceSuite(t *testing.T) {
tester.RunOnAny(
t,
tester.CorsoCITests,
tester.CorsoGraphConnectorTests,
tester.CorsoGraphConnectorExchangeTests)
suite.Run(t, new(ExchangeServiceSuite))
}
func (suite *ExchangeServiceSuite) SetupSuite() {
t := suite.T()
tester.MustGetEnvSets(t, tester.M365AcctCredEnvs)
a := tester.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err)
suite.credentials = m365
adpt, err := graph.CreateAdapter(
m365.AzureTenantID,
m365.AzureClientID,
m365.AzureClientSecret)
require.NoError(t, err)
suite.gs = graph.NewService(adpt)
}
func (suite *ExchangeServiceSuite) TestOptionsForCalendars() {
tests := []struct {
name string
params []string
checkError assert.ErrorAssertionFunc
}{
{
name: "Empty Literal",
params: []string{},
checkError: assert.NoError,
},
{
name: "Invalid Parameter",
params: []string{"status"},
checkError: assert.Error,
},
{
name: "Invalid Parameters",
params: []string{"status", "height", "month"},
checkError: assert.Error,
},
{
name: "Valid Parameters",
params: []string{"changeKey", "events", "owner"},
checkError: assert.NoError,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
_, err := optionsForCalendars(test.params)
test.checkError(t, err)
})
}
}
// 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) TestOptionsForFolders() {
tests := []struct {
name string
params []string
checkError assert.ErrorAssertionFunc
expected int
}{
{
name: "Valid Folder Option",
params: []string{"parentFolderId"},
checkError: assert.NoError,
expected: 2,
},
{
name: "Multiple Folder Options: Valid",
params: []string{"displayName", "isHidden"},
checkError: assert.NoError,
expected: 3,
},
{
name: "Invalid Folder option param",
params: []string{"status"},
checkError: assert.Error,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
config, err := optionsForMailFolders(test.params)
test.checkError(t, err)
if err == nil {
suite.Equal(test.expected, len(config.QueryParameters.Select))
}
})
}
}
// 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))
}
})
}
}
// TestGraphQueryFunctions verifies if Query functions APIs
// through Microsoft Graph are functional
func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() {
ctx, flush := tester.NewContext()
defer flush()
userID := tester.M365UserID(suite.T())
tests := []struct {
name string
function GraphQuery
}{
{
name: "GraphQuery: Get All ContactFolders",
function: GetAllContactFolderNamesForUser,
},
{
name: "GraphQuery: Get All Calendars for User",
function: GetAllCalendarNamesForUser,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
response, err := test.function(ctx, suite.gs, userID)
assert.NoError(t, err)
assert.NotNil(t, response)
})
}
}

View File

@ -0,0 +1,203 @@
package api
import (
"context"
"github.com/hashicorp/go-multierror"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
)
// CreateContactFolder makes a contact folder with the displayName of folderName.
// If successful, returns the created folder object.
func CreateContactFolder(
ctx context.Context,
gs graph.Servicer,
user, folderName string,
) (models.ContactFolderable, error) {
requestBody := models.NewContactFolder()
temp := folderName
requestBody.SetDisplayName(&temp)
return gs.Client().UsersById(user).ContactFolders().Post(ctx, requestBody, nil)
}
// DeleteContactFolder deletes the ContactFolder associated with the M365 ID if permissions are valid.
// Errors returned if the function call was not successful.
func DeleteContactFolder(ctx context.Context, gs graph.Servicer, user, folderID string) error {
return gs.Client().UsersById(user).ContactFoldersById(folderID).Delete(ctx, nil)
}
// RetrieveContactDataForUser is a GraphRetrievalFun that returns all associated fields.
func RetrieveContactDataForUser(
ctx context.Context,
gs graph.Servicer,
user, m365ID string,
) (serialization.Parsable, error) {
return gs.Client().UsersById(user).ContactsById(m365ID).Get(ctx, nil)
}
// GetAllContactFolderNamesForUser is a GraphQuery function for getting
// ContactFolderId and display names for contacts. All other information is omitted.
// Does not return the default Contact Folder
func GetAllContactFolderNamesForUser(
ctx context.Context,
gs graph.Servicer,
user string,
) (serialization.Parsable, error) {
options, err := optionsForContactFolders([]string{"displayName", "parentFolderId"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).ContactFolders().Get(ctx, options)
}
func GetContactFolderByID(
ctx context.Context,
gs graph.Servicer,
userID, dirID string,
optionalFields ...string,
) (models.ContactFolderable, error) {
fields := append([]string{"displayName", "parentFolderId"}, optionalFields...)
ofcf, err := optionsForContactFolderByID(fields)
if err != nil {
return nil, errors.Wrapf(err, "options for contact folder: %v", fields)
}
return gs.Client().
UsersById(userID).
ContactFoldersById(dirID).
Get(ctx, ofcf)
}
// TODO: we want this to be the full handler, not only the builder.
// but this halfway point minimizes changes for now.
func GetContactChildFoldersBuilder(
ctx context.Context,
gs graph.Servicer,
userID, baseDirID string,
optionalFields ...string,
) (
*users.ItemContactFoldersItemChildFoldersRequestBuilder,
*users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration,
error,
) {
fields := append([]string{"displayName", "parentFolderId"}, optionalFields...)
ofcf, err := optionsForContactChildFolders(fields)
if err != nil {
return nil, nil, errors.Wrapf(err, "options for contact child folders: %v", fields)
}
builder := gs.Client().
UsersById(userID).
ContactFoldersById(baseDirID).
ChildFolders()
return builder, ofcf, nil
}
// FetchContactIDsFromDirectory function that returns a list of all the m365IDs of the contacts
// of the targeted directory
func FetchContactIDsFromDirectory(
ctx context.Context,
gs graph.Servicer,
user, directoryID, oldDelta string,
) ([]string, []string, DeltaUpdate, error) {
var (
errs *multierror.Error
ids []string
removedIDs []string
deltaURL string
resetDelta bool
)
options, err := optionsForContactFoldersItemDelta([]string{"parentFolderId"})
if err != nil {
return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options")
}
getIDs := func(builder *users.ItemContactFoldersItemContactsDeltaRequestBuilder) error {
for {
resp, err := builder.Get(ctx, options)
if err != nil {
if err := graph.IsErrDeletedInFlight(err); err != nil {
return err
}
if err := graph.IsErrInvalidDelta(err); err != nil {
return err
}
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
for _, item := range resp.GetValue() {
if item.GetId() == nil {
errs = multierror.Append(
errs,
errors.Errorf("item with nil ID in folder %s", directoryID),
)
// TODO(ashmrtn): Handle fail-fast.
continue
}
if item.GetAdditionalData()[graph.AddtlDataRemoved] == nil {
ids = append(ids, *item.GetId())
} else {
removedIDs = append(removedIDs, *item.GetId())
}
}
delta := resp.GetOdataDeltaLink()
if delta != nil && len(*delta) > 0 {
deltaURL = *delta
}
nextLink := resp.GetOdataNextLink()
if nextLink == nil || len(*nextLink) == 0 {
break
}
builder = users.NewItemContactFoldersItemContactsDeltaRequestBuilder(*nextLink, gs.Adapter())
}
return nil
}
if len(oldDelta) > 0 {
err := getIDs(users.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, gs.Adapter()))
// note: happy path, not the error condition
if err == nil {
return ids, removedIDs, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil()
}
// only return on error if it is NOT a delta issue.
// otherwise we'll retry the call with the regular builder
if graph.IsErrInvalidDelta(err) == nil {
return nil, nil, DeltaUpdate{}, err
}
resetDelta = true
errs = nil
}
builder := gs.Client().
UsersById(user).
ContactFoldersById(directoryID).
Contacts().
Delta()
if err := getIDs(builder); err != nil {
return nil, nil, DeltaUpdate{}, err
}
return ids, removedIDs, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil()
}

View File

@ -0,0 +1,129 @@
package api
import (
"context"
"github.com/hashicorp/go-multierror"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
)
// CreateCalendar makes an event Calendar with the name in the user's M365 exchange account
// Reference: https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=go
func CreateCalendar(ctx context.Context, gs graph.Servicer, user, calendarName string) (models.Calendarable, error) {
requestbody := models.NewCalendar()
requestbody.SetName(&calendarName)
return gs.Client().UsersById(user).Calendars().Post(ctx, requestbody, nil)
}
// DeleteCalendar removes calendar from user's M365 account
// Reference: https://docs.microsoft.com/en-us/graph/api/calendar-delete?view=graph-rest-1.0&tabs=go
func DeleteCalendar(ctx context.Context, gs graph.Servicer, user, calendarID string) error {
return gs.Client().UsersById(user).CalendarsById(calendarID).Delete(ctx, nil)
}
// RetrieveEventDataForUser is a GraphRetrievalFunc that returns event data.
// Calendarable and attachment fields are omitted due to size
func RetrieveEventDataForUser(
ctx context.Context,
gs graph.Servicer,
user, m365ID string,
) (serialization.Parsable, error) {
return gs.Client().UsersById(user).EventsById(m365ID).Get(ctx, nil)
}
func GetAllCalendarNamesForUser(ctx context.Context, gs graph.Servicer, user string) (serialization.Parsable, error) {
options, err := optionsForCalendars([]string{"name", "owner"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).Calendars().Get(ctx, options)
}
// TODO: we want this to be the full handler, not only the builder.
// but this halfway point minimizes changes for now.
func GetCalendarsBuilder(
ctx context.Context,
gs graph.Servicer,
userID string,
optionalFields ...string,
) (
*users.ItemCalendarsRequestBuilder,
*users.ItemCalendarsRequestBuilderGetRequestConfiguration,
error,
) {
ofcf, err := optionsForCalendars(optionalFields)
if err != nil {
return nil, nil, errors.Wrapf(err, "options for event calendars: %v", optionalFields)
}
builder := gs.Client().
UsersById(userID).
Calendars()
return builder, ofcf, nil
}
// FetchEventIDsFromCalendar returns a list of all M365IDs of events of the targeted Calendar.
func FetchEventIDsFromCalendar(
ctx context.Context,
gs graph.Servicer,
user, calendarID, oldDelta string,
) ([]string, []string, DeltaUpdate, error) {
var (
errs *multierror.Error
ids []string
)
options, err := optionsForEventsByCalendar([]string{"id"})
if err != nil {
return nil, nil, DeltaUpdate{}, err
}
builder := gs.Client().
UsersById(user).
CalendarsById(calendarID).
Events()
for {
resp, err := builder.Get(ctx, options)
if err != nil {
if err := graph.IsErrDeletedInFlight(err); err != nil {
return nil, nil, DeltaUpdate{}, err
}
return nil, nil, DeltaUpdate{}, errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
for _, item := range resp.GetValue() {
if item.GetId() == nil {
errs = multierror.Append(
errs,
errors.Errorf("event with nil ID in calendar %s", calendarID),
)
// TODO(ashmrtn): Handle fail-fast.
continue
}
ids = append(ids, *item.GetId())
}
nextLink := resp.GetOdataNextLink()
if nextLink == nil || len(*nextLink) == 0 {
break
}
builder = users.NewItemCalendarsItemEventsRequestBuilder(*nextLink, gs.Adapter())
}
// Events don't have a delta endpoint so just return an empty string.
return ids, nil, DeltaUpdate{}, errs.ErrorOrNil()
}

View File

@ -0,0 +1,189 @@
package api
import (
"context"
"github.com/hashicorp/go-multierror"
"github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support"
)
// CreateMailFolder makes a mail folder iff a folder of the same name does not exist
// Reference: https://docs.microsoft.com/en-us/graph/api/user-post-mailfolders?view=graph-rest-1.0&tabs=http
func CreateMailFolder(ctx context.Context, gs graph.Servicer, user, folder string) (models.MailFolderable, error) {
isHidden := false
requestBody := models.NewMailFolder()
requestBody.SetDisplayName(&folder)
requestBody.SetIsHidden(&isHidden)
return gs.Client().UsersById(user).MailFolders().Post(ctx, requestBody, nil)
}
func CreateMailFolderWithParent(
ctx context.Context,
gs graph.Servicer,
user, folder, parentID string,
) (models.MailFolderable, error) {
isHidden := false
requestBody := models.NewMailFolder()
requestBody.SetDisplayName(&folder)
requestBody.SetIsHidden(&isHidden)
return gs.Client().
UsersById(user).
MailFoldersById(parentID).
ChildFolders().
Post(ctx, requestBody, nil)
}
// DeleteMailFolder removes a mail folder with the corresponding M365 ID from the user's M365 Exchange account
// Reference: https://docs.microsoft.com/en-us/graph/api/mailfolder-delete?view=graph-rest-1.0&tabs=http
func DeleteMailFolder(ctx context.Context, gs graph.Servicer, user, folderID string) error {
return gs.Client().UsersById(user).MailFoldersById(folderID).Delete(ctx, nil)
}
// RetrieveMessageDataForUser is a GraphRetrievalFunc that returns message data.
// Attachment field is omitted due to size.
func RetrieveMessageDataForUser(
ctx context.Context,
gs graph.Servicer,
user, m365ID string,
) (serialization.Parsable, error) {
return gs.Client().UsersById(user).MessagesById(m365ID).Get(ctx, nil)
}
// GetMailFoldersBuilder retrieves all of the users current mail folders.
// Folder hierarchy is represented in its current state, and does
// not contain historical data.
// TODO: we want this to be the full handler, not only the builder.
// but this halfway point minimizes changes for now.
func GetAllMailFoldersBuilder(
ctx context.Context,
gs graph.Servicer,
userID string,
) *users.ItemMailFoldersDeltaRequestBuilder {
return gs.Client().
UsersById(userID).
MailFolders().
Delta()
}
func GetMailFolderByID(
ctx context.Context,
gs graph.Servicer,
userID, dirID string,
optionalFields ...string,
) (models.MailFolderable, error) {
ofmf, err := optionsForMailFoldersItem(optionalFields)
if err != nil {
return nil, errors.Wrapf(err, "options for mail folder: %v", optionalFields)
}
return gs.Client().
UsersById(userID).
MailFoldersById(dirID).
Get(ctx, ofmf)
}
// FetchMessageIDsFromDirectory function that returns a list of all the m365IDs of the exchange.Mail
// of the targeted directory
func FetchMessageIDsFromDirectory(
ctx context.Context,
gs graph.Servicer,
user, directoryID, oldDelta string,
) ([]string, []string, DeltaUpdate, error) {
var (
errs *multierror.Error
ids []string
removedIDs []string
deltaURL string
resetDelta bool
)
options, err := optionsForFolderMessagesDelta([]string{"isRead"})
if err != nil {
return nil, nil, DeltaUpdate{}, errors.Wrap(err, "getting query options")
}
getIDs := func(builder *users.ItemMailFoldersItemMessagesDeltaRequestBuilder) error {
for {
resp, err := builder.Get(ctx, options)
if err != nil {
if err := graph.IsErrDeletedInFlight(err); err != nil {
return err
}
if err := graph.IsErrInvalidDelta(err); err != nil {
return err
}
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
for _, item := range resp.GetValue() {
if item.GetId() == nil {
errs = multierror.Append(
errs,
errors.Errorf("item with nil ID in folder %s", directoryID),
)
// TODO(ashmrtn): Handle fail-fast.
continue
}
if item.GetAdditionalData()[graph.AddtlDataRemoved] == nil {
ids = append(ids, *item.GetId())
} else {
removedIDs = append(removedIDs, *item.GetId())
}
}
delta := resp.GetOdataDeltaLink()
if delta != nil && len(*delta) > 0 {
deltaURL = *delta
}
nextLink := resp.GetOdataNextLink()
if nextLink == nil || len(*nextLink) == 0 {
break
}
builder = users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(*nextLink, gs.Adapter())
}
return nil
}
if len(oldDelta) > 0 {
err := getIDs(users.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, gs.Adapter()))
// note: happy path, not the error condition
if err == nil {
return ids, removedIDs, DeltaUpdate{deltaURL, false}, errs.ErrorOrNil()
}
// only return on error if it is NOT a delta issue.
// otherwise we'll retry the call with the regular builder
if graph.IsErrInvalidDelta(err) == nil {
return nil, nil, DeltaUpdate{}, err
}
resetDelta = true
errs = nil
}
builder := gs.Client().
UsersById(user).
MailFoldersById(directoryID).
Messages().
Delta()
if err := getIDs(builder); err != nil {
return nil, nil, DeltaUpdate{}, err
}
return ids, removedIDs, DeltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil()
}

View File

@ -1,9 +1,9 @@
package exchange package api
import ( import (
"fmt" "fmt"
msuser "github.com/microsoftgraph/msgraph-sdk-go/users" "github.com/microsoftgraph/msgraph-sdk-go/users"
) )
// ----------------------------------------------------------------------- // -----------------------------------------------------------------------
@ -74,16 +74,16 @@ var (
func optionsForFolderMessagesDelta( func optionsForFolderMessagesDelta(
moreOps []string, moreOps []string,
) (*msuser.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForMessages) selecting, err := buildOptions(moreOps, fieldsForMessages)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemMailFoldersItemMessagesDeltaRequestBuilderGetQueryParameters{ requestParameters := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration{ options := &users.ItemMailFoldersItemMessagesDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -94,7 +94,7 @@ func optionsForFolderMessagesDelta(
// @param moreOps should reflect elements from fieldsForCalendars // @param moreOps should reflect elements from fieldsForCalendars
// @return is first call in Calendars().GetWithRequestConfigurationAndResponseHandler // @return is first call in Calendars().GetWithRequestConfigurationAndResponseHandler
func optionsForCalendars(moreOps []string) ( func optionsForCalendars(moreOps []string) (
*msuser.ItemCalendarsRequestBuilderGetRequestConfiguration, *users.ItemCalendarsRequestBuilderGetRequestConfiguration,
error, error,
) { ) {
selecting, err := buildOptions(moreOps, fieldsForCalendars) selecting, err := buildOptions(moreOps, fieldsForCalendars)
@ -102,10 +102,10 @@ func optionsForCalendars(moreOps []string) (
return nil, err return nil, err
} }
// should be a CalendarsRequestBuilderGetRequestConfiguration // should be a CalendarsRequestBuilderGetRequestConfiguration
requestParams := &msuser.ItemCalendarsRequestBuilderGetQueryParameters{ requestParams := &users.ItemCalendarsRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemCalendarsRequestBuilderGetRequestConfiguration{ options := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{
QueryParameters: requestParams, QueryParameters: requestParams,
} }
@ -115,7 +115,7 @@ func optionsForCalendars(moreOps []string) (
// optionsForContactFolders places allowed options for exchange.ContactFolder object // optionsForContactFolders places allowed options for exchange.ContactFolder object
// @return is first call in ContactFolders().GetWithRequestConfigurationAndResponseHandler // @return is first call in ContactFolders().GetWithRequestConfigurationAndResponseHandler
func optionsForContactFolders(moreOps []string) ( func optionsForContactFolders(moreOps []string) (
*msuser.ItemContactFoldersRequestBuilderGetRequestConfiguration, *users.ItemContactFoldersRequestBuilderGetRequestConfiguration,
error, error,
) { ) {
selecting, err := buildOptions(moreOps, fieldsForFolders) selecting, err := buildOptions(moreOps, fieldsForFolders)
@ -123,10 +123,10 @@ func optionsForContactFolders(moreOps []string) (
return nil, err return nil, err
} }
requestParameters := &msuser.ItemContactFoldersRequestBuilderGetQueryParameters{ requestParameters := &users.ItemContactFoldersRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemContactFoldersRequestBuilderGetRequestConfiguration{ options := &users.ItemContactFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -134,7 +134,7 @@ func optionsForContactFolders(moreOps []string) (
} }
func optionsForContactFolderByID(moreOps []string) ( func optionsForContactFolderByID(moreOps []string) (
*msuser.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration, *users.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration,
error, error,
) { ) {
selecting, err := buildOptions(moreOps, fieldsForFolders) selecting, err := buildOptions(moreOps, fieldsForFolders)
@ -142,10 +142,10 @@ func optionsForContactFolderByID(moreOps []string) (
return nil, err return nil, err
} }
requestParameters := &msuser.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{ requestParameters := &users.ItemContactFoldersContactFolderItemRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration{ options := &users.ItemContactFoldersContactFolderItemRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -157,16 +157,16 @@ func optionsForContactFolderByID(moreOps []string) (
// @return is first call in MailFolders().GetWithRequestConfigurationAndResponseHandler(options, handler) // @return is first call in MailFolders().GetWithRequestConfigurationAndResponseHandler(options, handler)
func optionsForMailFolders( func optionsForMailFolders(
moreOps []string, moreOps []string,
) (*msuser.ItemMailFoldersRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemMailFoldersRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForFolders) selecting, err := buildOptions(moreOps, fieldsForFolders)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemMailFoldersRequestBuilderGetQueryParameters{ requestParameters := &users.ItemMailFoldersRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemMailFoldersRequestBuilderGetRequestConfiguration{ options := &users.ItemMailFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -178,16 +178,16 @@ func optionsForMailFolders(
// Returns first call in MailFoldersById().GetWithRequestConfigurationAndResponseHandler(options, handler) // Returns first call in MailFoldersById().GetWithRequestConfigurationAndResponseHandler(options, handler)
func optionsForMailFoldersItem( func optionsForMailFoldersItem(
moreOps []string, moreOps []string,
) (*msuser.ItemMailFoldersMailFolderItemRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemMailFoldersMailFolderItemRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForFolders) selecting, err := buildOptions(moreOps, fieldsForFolders)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemMailFoldersMailFolderItemRequestBuilderGetQueryParameters{ requestParameters := &users.ItemMailFoldersMailFolderItemRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemMailFoldersMailFolderItemRequestBuilderGetRequestConfiguration{ options := &users.ItemMailFoldersMailFolderItemRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -196,35 +196,17 @@ func optionsForMailFoldersItem(
func optionsForContactFoldersItemDelta( func optionsForContactFoldersItemDelta(
moreOps []string, moreOps []string,
) (*msuser.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForContacts) selecting, err := buildOptions(moreOps, fieldsForContacts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemContactFoldersItemContactsDeltaRequestBuilderGetQueryParameters{ requestParameters := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{ options := &users.ItemContactFoldersItemContactsDeltaRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters,
}
return options, nil
}
// optionsForEvents ensures valid option inputs for exchange.Events
// @return is first call in Events().GetWithRequestConfigurationAndResponseHandler(options, handler)
func optionsForEvents(moreOps []string) (*msuser.ItemEventsRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForEvents)
if err != nil {
return nil, err
}
requestParameters := &msuser.ItemEventsRequestBuilderGetQueryParameters{
Select: selecting,
}
options := &msuser.ItemEventsRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -234,17 +216,17 @@ func optionsForEvents(moreOps []string) (*msuser.ItemEventsRequestBuilderGetRequ
// optionsForEvents ensures a valid option inputs for `exchange.Events` when selected from within a Calendar // optionsForEvents ensures a valid option inputs for `exchange.Events` when selected from within a Calendar
func optionsForEventsByCalendar( func optionsForEventsByCalendar(
moreOps []string, moreOps []string,
) (*msuser.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForEvents) selecting, err := buildOptions(moreOps, fieldsForEvents)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemCalendarsItemEventsRequestBuilderGetQueryParameters{ requestParameters := &users.ItemCalendarsItemEventsRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{ options := &users.ItemCalendarsItemEventsRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -254,16 +236,16 @@ func optionsForEventsByCalendar(
// optionsForContactChildFolders builds a contacts child folders request. // optionsForContactChildFolders builds a contacts child folders request.
func optionsForContactChildFolders( func optionsForContactChildFolders(
moreOps []string, moreOps []string,
) (*msuser.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration, error) { ) (*users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForContacts) selecting, err := buildOptions(moreOps, fieldsForContacts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemContactFoldersItemChildFoldersRequestBuilderGetQueryParameters{ requestParameters := &users.ItemContactFoldersItemChildFoldersRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{ options := &users.ItemContactFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }
@ -272,16 +254,16 @@ func optionsForContactChildFolders(
// optionsForContacts transforms options into select query for MailContacts // optionsForContacts transforms options into select query for MailContacts
// @return is the first call in Contacts().GetWithRequestConfigurationAndResponseHandler(options, handler) // @return is the first call in Contacts().GetWithRequestConfigurationAndResponseHandler(options, handler)
func optionsForContacts(moreOps []string) (*msuser.ItemContactsRequestBuilderGetRequestConfiguration, error) { func optionsForContacts(moreOps []string) (*users.ItemContactsRequestBuilderGetRequestConfiguration, error) {
selecting, err := buildOptions(moreOps, fieldsForContacts) selecting, err := buildOptions(moreOps, fieldsForContacts)
if err != nil { if err != nil {
return nil, err return nil, err
} }
requestParameters := &msuser.ItemContactsRequestBuilderGetQueryParameters{ requestParameters := &users.ItemContactsRequestBuilderGetQueryParameters{
Select: selecting, Select: selecting,
} }
options := &msuser.ItemContactsRequestBuilderGetRequestConfiguration{ options := &users.ItemContactsRequestBuilderGetRequestConfiguration{
QueryParameters: requestParameters, QueryParameters: requestParameters,
} }

View File

@ -6,6 +6,7 @@ import (
msuser "github.com/microsoftgraph/msgraph-sdk-go/users" msuser "github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -24,19 +25,7 @@ func (cfc *contactFolderCache) populateContactRoot(
directoryID string, directoryID string,
baseContainerPath []string, baseContainerPath []string,
) error { ) error {
wantedOpts := []string{"displayName", "parentFolderId"} f, err := api.GetContactFolderByID(ctx, cfc.gs, cfc.userID, directoryID)
opts, err := optionsForContactFolderByID(wantedOpts)
if err != nil {
return errors.Wrapf(err, "getting options for contact folder cache: %v", wantedOpts)
}
f, err := cfc.
gs.
Client().
UsersById(cfc.userID).
ContactFoldersById(directoryID).
Get(ctx, opts)
if err != nil { if err != nil {
return errors.Wrapf( return errors.Wrapf(
err, err,
@ -65,21 +54,17 @@ func (cfc *contactFolderCache) Populate(
return err return err
} }
var ( var errs error
errs error
options, err = optionsForContactChildFolders([]string{"displayName", "parentFolderId"})
)
builder, options, err := api.GetContactChildFoldersBuilder(
ctx,
cfc.gs,
cfc.userID,
baseID)
if err != nil { if err != nil {
return errors.Wrap(err, "contact cache resolver option") return errors.Wrap(err, "contact cache resolver option")
} }
builder := cfc.
gs.Client().
UsersById(cfc.userID).
ContactFoldersById(baseID).
ChildFolders()
for { for {
resp, err := builder.Get(ctx, options) resp, err := builder.Get(ctx, options)
if err != nil { if err != nil {

View File

@ -11,9 +11,14 @@ import (
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
) )
// ---------------------------------------------------------------------------
// mocks and helpers
// ---------------------------------------------------------------------------
type mockContainer struct { type mockContainer struct {
id *string id *string
name *string name *string
@ -34,6 +39,10 @@ func (m mockContainer) GetParentFolderId() *string {
return m.parentID return m.parentID
} }
// ---------------------------------------------------------------------------
// unit suite
// ---------------------------------------------------------------------------
type FolderCacheUnitSuite struct { type FolderCacheUnitSuite struct {
suite.Suite suite.Suite
} }
@ -284,6 +293,10 @@ func resolverWithContainers(numContainers int) (*containerResolver, []*mockCache
return resolver, containers return resolver, containers
} }
// ---------------------------------------------------------------------------
// configured unit suite
// ---------------------------------------------------------------------------
// TestConfiguredFolderCacheUnitSuite cannot run its tests in parallel. // TestConfiguredFolderCacheUnitSuite cannot run its tests in parallel.
type ConfiguredFolderCacheUnitSuite struct { type ConfiguredFolderCacheUnitSuite struct {
suite.Suite suite.Suite
@ -431,3 +444,182 @@ func (suite *ConfiguredFolderCacheUnitSuite) TestAddToCache() {
require.NoError(t, err) require.NoError(t, err)
assert.Equal(t, m.expectedPath, p.String()) assert.Equal(t, m.expectedPath, p.String())
} }
// ---------------------------------------------------------------------------
// integration suite
// ---------------------------------------------------------------------------
type FolderCacheIntegrationSuite struct {
suite.Suite
credentials account.M365Config
gs graph.Servicer
}
func TestFolderCacheIntegrationSuite(t *testing.T) {
tester.RunOnAny(
t,
tester.CorsoCITests,
tester.CorsoConnectorExchangeFolderCacheTests)
suite.Run(t, new(FolderCacheIntegrationSuite))
}
func (suite *FolderCacheIntegrationSuite) SetupSuite() {
t := suite.T()
tester.MustGetEnvSets(t, tester.M365AcctCredEnvs)
a := tester.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err)
suite.credentials = m365
adpt, err := graph.CreateAdapter(
m365.AzureTenantID,
m365.AzureClientID,
m365.AzureClientSecret)
require.NoError(t, err)
suite.gs = graph.NewService(adpt)
require.NoError(suite.T(), err)
}
// Testing to ensure that cache system works for in multiple different environments
func (suite *FolderCacheIntegrationSuite) TestCreateContainerDestination() {
ctx, flush := tester.NewContext()
defer flush()
a := tester.NewM365Account(suite.T())
m365, err := a.M365Config()
require.NoError(suite.T(), err)
connector, err := createService(m365)
require.NoError(suite.T(), err)
var (
user = tester.M365UserID(suite.T())
directoryCaches = make(map[path.CategoryType]graph.ContainerResolver)
folderName = tester.DefaultTestRestoreDestination().ContainerName
tests = []struct {
name string
pathFunc1 func(t *testing.T) path.Path
pathFunc2 func(t *testing.T) path.Path
category path.CategoryType
}{
{
name: "Mail Cache Test",
category: path.EmailCategory,
pathFunc1: func(t *testing.T) path.Path {
pth, err := path.Builder{}.Append("Griffindor").
Append("Croix").ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EmailCategory,
false,
)
require.NoError(t, err)
return pth
},
pathFunc2: func(t *testing.T) path.Path {
pth, err := path.Builder{}.Append("Griffindor").
Append("Felicius").ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EmailCategory,
false,
)
require.NoError(t, err)
return pth
},
},
{
name: "Contact Cache Test",
category: path.ContactsCategory,
pathFunc1: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("HufflePuff").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.ContactsCategory,
false,
)
require.NoError(t, err)
return aPath
},
pathFunc2: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("Ravenclaw").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.ContactsCategory,
false,
)
require.NoError(t, err)
return aPath
},
},
{
name: "Event Cache Test",
category: path.EventsCategory,
pathFunc1: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("Durmstrang").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EventsCategory,
false,
)
require.NoError(t, err)
return aPath
},
pathFunc2: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("Beauxbatons").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EventsCategory,
false,
)
require.NoError(t, err)
return aPath
},
},
}
)
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
folderID, err := CreateContainerDestinaion(
ctx,
connector,
test.pathFunc1(t),
folderName,
directoryCaches,
)
require.NoError(t, err)
resolver := directoryCaches[test.category]
_, err = resolver.IDToPath(ctx, folderID)
assert.NoError(t, err)
secondID, err := CreateContainerDestinaion(
ctx,
connector,
test.pathFunc2(t),
folderName,
directoryCaches,
)
require.NoError(t, err)
_, err = resolver.IDToPath(ctx, secondID)
require.NoError(t, err)
_, ok := resolver.PathInCache(folderName)
require.True(t, ok)
})
}
}

View File

@ -6,6 +6,7 @@ import (
msuser "github.com/microsoftgraph/msgraph-sdk-go/users" msuser "github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -31,7 +32,7 @@ func (ecc *eventCalendarCache) Populate(
ecc.containerResolver = newContainerResolver() ecc.containerResolver = newContainerResolver()
} }
options, err := optionsForCalendars([]string{"name"}) builder, options, err := api.GetCalendarsBuilder(ctx, ecc.gs, ecc.userID, "name")
if err != nil { if err != nil {
return err return err
} }
@ -41,8 +42,6 @@ func (ecc *eventCalendarCache) Populate(
directories = make([]graph.Container, 0) directories = make([]graph.Container, 0)
) )
builder := ecc.gs.Client().UsersById(ecc.userID).Calendars()
for { for {
resp, err := builder.Get(ctx, options) resp, err := builder.Get(ctx, options)
if err != nil { if err != nil {

View File

@ -18,6 +18,7 @@ import (
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -135,14 +136,14 @@ func (col *Collection) Items() <-chan data.Stream {
// GetQueryAndSerializeFunc helper function that returns the two functions functions // GetQueryAndSerializeFunc helper function that returns the two functions functions
// required to convert M365 identifier into a byte array filled with the serialized data // required to convert M365 identifier into a byte array filled with the serialized data
func GetQueryAndSerializeFunc(category path.CategoryType) (GraphRetrievalFunc, GraphSerializeFunc) { func GetQueryAndSerializeFunc(category path.CategoryType) (api.GraphRetrievalFunc, GraphSerializeFunc) {
switch category { switch category {
case path.ContactsCategory: case path.ContactsCategory:
return RetrieveContactDataForUser, serializeAndStreamContact return api.RetrieveContactDataForUser, serializeAndStreamContact
case path.EventsCategory: case path.EventsCategory:
return RetrieveEventDataForUser, serializeAndStreamEvent return api.RetrieveEventDataForUser, serializeAndStreamEvent
case path.EmailCategory: case path.EmailCategory:
return RetrieveMessageDataForUser, serializeAndStreamMessage return api.RetrieveMessageDataForUser, serializeAndStreamMessage
// Unsupported options returns nil, nil // Unsupported options returns nil, nil
default: default:
return nil, nil return nil, nil

View File

@ -1,599 +0,0 @@
package exchange
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
)
type ExchangeServiceSuite struct {
suite.Suite
credentials account.M365Config
gs graph.Servicer
}
func TestExchangeServiceSuite(t *testing.T) {
tester.RunOnAny(
t,
tester.CorsoCITests,
tester.CorsoGraphConnectorTests,
tester.CorsoGraphConnectorExchangeTests)
suite.Run(t, new(ExchangeServiceSuite))
}
func (suite *ExchangeServiceSuite) SetupSuite() {
t := suite.T()
tester.MustGetEnvSets(t, tester.M365AcctCredEnvs)
a := tester.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err)
service, err := createService(m365)
require.NoError(t, err)
suite.credentials = m365
suite.gs = 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.credentials
invalidCredentials := suite.credentials
invalidCredentials.AzureClientSecret = ""
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.AzureClientSecret)
_, err := createService(test.credentials)
test.checkErr(t, err)
})
}
}
func (suite *ExchangeServiceSuite) TestOptionsForCalendars() {
tests := []struct {
name string
params []string
checkError assert.ErrorAssertionFunc
}{
{
name: "Empty Literal",
params: []string{},
checkError: assert.NoError,
},
{
name: "Invalid Parameter",
params: []string{"status"},
checkError: assert.Error,
},
{
name: "Invalid Parameters",
params: []string{"status", "height", "month"},
checkError: assert.Error,
},
{
name: "Valid Parameters",
params: []string{"changeKey", "events", "owner"},
checkError: assert.NoError,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
_, err := optionsForCalendars(test.params)
test.checkError(t, err)
})
}
}
// 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) TestOptionsForFolders() {
tests := []struct {
name string
params []string
checkError assert.ErrorAssertionFunc
expected int
}{
{
name: "Valid Folder Option",
params: []string{"parentFolderId"},
checkError: assert.NoError,
expected: 2,
},
{
name: "Multiple Folder Options: Valid",
params: []string{"displayName", "isHidden"},
checkError: assert.NoError,
expected: 3,
},
{
name: "Invalid Folder option param",
params: []string{"status"},
checkError: assert.Error,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
config, err := optionsForMailFolders(test.params)
test.checkError(t, err)
if err == nil {
suite.Equal(test.expected, len(config.QueryParameters.Select))
}
})
}
}
// 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))
}
})
}
}
// TestGraphQueryFunctions verifies if Query functions APIs
// through Microsoft Graph are functional
func (suite *ExchangeServiceSuite) TestGraphQueryFunctions() {
ctx, flush := tester.NewContext()
defer flush()
userID := tester.M365UserID(suite.T())
tests := []struct {
name string
function GraphQuery
}{
{
name: "GraphQuery: Get All Contacts For User",
function: GetAllContactsForUser,
},
{
name: "GraphQuery: Get All Folders",
function: GetAllFolderNamesForUser,
},
{
name: "GraphQuery: Get All ContactFolders",
function: GetAllContactFolderNamesForUser,
},
{
name: "GraphQuery: Get Default ContactFolder",
function: GetDefaultContactFolderForUser,
},
{
name: "GraphQuery: Get All Events for User",
function: GetAllEventsForUser,
},
{
name: "GraphQuery: Get All Calendars for User",
function: GetAllCalendarNamesForUser,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
response, err := test.function(ctx, suite.gs, userID)
assert.NoError(t, err)
assert.NotNil(t, response)
})
}
}
//==========================
// Restore Functions
//==========================
// TestRestoreContact ensures contact object can be created, placed into
// the Corso Folder. The function handles test clean-up.
func (suite *ExchangeServiceSuite) TestRestoreContact() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
now = time.Now()
folderName = "TestRestoreContact: " + common.FormatSimpleDateTime(now)
)
aFolder, err := CreateContactFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
folderID := *aFolder.GetId()
defer func() {
// Remove the folder containing contact prior to exiting test
err = DeleteContactFolder(ctx, suite.gs, userID, folderID)
assert.NoError(t, err)
}()
info, err := RestoreExchangeContact(ctx,
mockconnector.GetMockContactBytes("Corso TestContact"),
suite.gs,
control.Copy,
folderID,
userID)
assert.NoError(t, err, support.ConnectorStackErrorTrace(err))
assert.NotNil(t, info, "contact item info")
}
// TestRestoreEvent verifies that event object is able to created
// and sent into the test account of the Corso user in the newly created Corso Calendar
func (suite *ExchangeServiceSuite) TestRestoreEvent() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
name = "TestRestoreEvent: " + common.FormatSimpleDateTime(time.Now())
)
calendar, err := CreateCalendar(ctx, suite.gs, userID, name)
require.NoError(t, err)
calendarID := *calendar.GetId()
defer func() {
// Removes calendar containing events created during the test
err = DeleteCalendar(ctx, suite.gs, userID, calendarID)
assert.NoError(t, err)
}()
info, err := RestoreExchangeEvent(ctx,
mockconnector.GetMockEventWithAttendeesBytes(name),
suite.gs,
control.Copy,
calendarID,
userID)
assert.NoError(t, err, support.ConnectorStackErrorTrace(err))
assert.NotNil(t, info, "event item info")
}
// TestRestoreExchangeObject verifies path.Category usage for restored objects
func (suite *ExchangeServiceSuite) TestRestoreExchangeObject() {
a := tester.NewM365Account(suite.T())
m365, err := a.M365Config()
require.NoError(suite.T(), err)
service, err := createService(m365)
require.NoError(suite.T(), err)
userID := tester.M365UserID(suite.T())
now := time.Now()
tests := []struct {
name string
bytes []byte
category path.CategoryType
cleanupFunc func(context.Context, graph.Servicer, string, string) error
destination func(*testing.T, context.Context) string
}{
{
name: "Test Mail",
bytes: mockconnector.GetMockMessageBytes("Restore Exchange Object"),
category: path.EmailCategory,
cleanupFunc: DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailObject: " + common.FormatSimpleDateTime(now)
folder, err := CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: One Direct Attachment",
bytes: mockconnector.GetMockMessageWithDirectAttachment("Restore 1 Attachment"),
category: path.EmailCategory,
cleanupFunc: DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithAttachment: " + common.FormatSimpleDateTime(now)
folder, err := CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: One Large Attachment",
bytes: mockconnector.GetMockMessageWithLargeAttachment("Restore Large Attachment"),
category: path.EmailCategory,
cleanupFunc: DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithLargeAttachment: " + common.FormatSimpleDateTime(now)
folder, err := CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: Two Attachments",
bytes: mockconnector.GetMockMessageWithTwoAttachments("Restore 2 Attachments"),
category: path.EmailCategory,
cleanupFunc: DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithAttachments: " + common.FormatSimpleDateTime(now)
folder, err := CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: Reference(OneDrive) Attachment",
bytes: mockconnector.GetMessageWithOneDriveAttachment("Restore Reference(OneDrive) Attachment"),
category: path.EmailCategory,
cleanupFunc: DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithReferenceAttachment: " + common.FormatSimpleDateTime(now)
folder, err := CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
// TODO: #884 - reinstate when able to specify root folder by name
{
name: "Test Contact",
bytes: mockconnector.GetMockContactBytes("Test_Omega"),
category: path.ContactsCategory,
cleanupFunc: DeleteContactFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreContactObject: " + common.FormatSimpleDateTime(now)
folder, err := CreateContactFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Events",
bytes: mockconnector.GetDefaultMockEventBytes("Restored Event Object"),
category: path.EventsCategory,
cleanupFunc: DeleteCalendar,
destination: func(t *testing.T, ctx context.Context) string {
calendarName := "TestRestoreEventObject: " + common.FormatSimpleDateTime(now)
calendar, err := CreateCalendar(ctx, suite.gs, userID, calendarName)
require.NoError(t, err)
return *calendar.GetId()
},
},
{
name: "Test Event with Attachment",
bytes: mockconnector.GetMockEventWithAttachment("Restored Event Attachment"),
category: path.EventsCategory,
cleanupFunc: DeleteCalendar,
destination: func(t *testing.T, ctx context.Context) string {
calendarName := "TestRestoreEventObject_" + common.FormatSimpleDateTime(now)
calendar, err := CreateCalendar(ctx, suite.gs, userID, calendarName)
require.NoError(t, err)
return *calendar.GetId()
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
ctx, flush := tester.NewContext()
defer flush()
destination := test.destination(t, ctx)
info, err := RestoreExchangeObject(
ctx,
test.bytes,
test.category,
control.Copy,
service,
destination,
userID,
)
assert.NoError(t, err, support.ConnectorStackErrorTrace(err))
assert.NotNil(t, info, "item info is populated")
cleanupError := test.cleanupFunc(ctx, service, userID, destination)
assert.NoError(t, cleanupError)
})
}
}
// Testing to ensure that cache system works for in multiple different environments
func (suite *ExchangeServiceSuite) TestGetContainerIDFromCache() {
ctx, flush := tester.NewContext()
defer flush()
a := tester.NewM365Account(suite.T())
m365, err := a.M365Config()
require.NoError(suite.T(), err)
connector, err := createService(m365)
require.NoError(suite.T(), err)
var (
user = tester.M365UserID(suite.T())
directoryCaches = make(map[path.CategoryType]graph.ContainerResolver)
folderName = tester.DefaultTestRestoreDestination().ContainerName
tests = []struct {
name string
pathFunc1 func(t *testing.T) path.Path
pathFunc2 func(t *testing.T) path.Path
category path.CategoryType
}{
{
name: "Mail Cache Test",
category: path.EmailCategory,
pathFunc1: func(t *testing.T) path.Path {
pth, err := path.Builder{}.Append("Griffindor").
Append("Croix").ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EmailCategory,
false,
)
require.NoError(t, err)
return pth
},
pathFunc2: func(t *testing.T) path.Path {
pth, err := path.Builder{}.Append("Griffindor").
Append("Felicius").ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EmailCategory,
false,
)
require.NoError(t, err)
return pth
},
},
{
name: "Contact Cache Test",
category: path.ContactsCategory,
pathFunc1: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("HufflePuff").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.ContactsCategory,
false,
)
require.NoError(t, err)
return aPath
},
pathFunc2: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("Ravenclaw").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.ContactsCategory,
false,
)
require.NoError(t, err)
return aPath
},
},
{
name: "Event Cache Test",
category: path.EventsCategory,
pathFunc1: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("Durmstrang").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EventsCategory,
false,
)
require.NoError(t, err)
return aPath
},
pathFunc2: func(t *testing.T) path.Path {
aPath, err := path.Builder{}.Append("Beauxbatons").
ToDataLayerExchangePathForCategory(
suite.credentials.AzureTenantID,
user,
path.EventsCategory,
false,
)
require.NoError(t, err)
return aPath
},
},
}
)
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
folderID, err := CreateContainerDestinaion(
ctx,
connector,
test.pathFunc1(t),
folderName,
directoryCaches)
require.NoError(t, err)
resolver := directoryCaches[test.category]
_, err = resolver.IDToPath(ctx, folderID)
assert.NoError(t, err)
secondID, err := CreateContainerDestinaion(
ctx,
connector,
test.pathFunc2(t),
folderName,
directoryCaches)
require.NoError(t, err)
_, err = resolver.IDToPath(ctx, secondID)
require.NoError(t, err)
_, ok := resolver.PathInCache(folderName)
require.True(t, ok)
})
}
}

View File

@ -10,6 +10,7 @@ import (
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
@ -83,7 +84,7 @@ func (suite *ExchangeIteratorSuite) TestCollectionFunctions() {
tests := []struct { tests := []struct {
name string name string
queryFunc GraphQuery queryFunc api.GraphQuery
scope selectors.ExchangeScope scope selectors.ExchangeScope
iterativeFunction func( iterativeFunction func(
container map[string]graph.Container, container map[string]graph.Container,
@ -93,13 +94,13 @@ func (suite *ExchangeIteratorSuite) TestCollectionFunctions() {
}{ }{
{ {
name: "Contacts Iterative Check", name: "Contacts Iterative Check",
queryFunc: GetAllContactFolderNamesForUser, queryFunc: api.GetAllContactFolderNamesForUser,
transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue, transformer: models.CreateContactFolderCollectionResponseFromDiscriminatorValue,
iterativeFunction: IterativeCollectContactContainers, iterativeFunction: IterativeCollectContactContainers,
}, },
{ {
name: "Events Iterative Check", name: "Events Iterative Check",
queryFunc: GetAllCalendarNamesForUser, queryFunc: api.GetAllCalendarNamesForUser,
transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue, transformer: models.CreateCalendarCollectionResponseFromDiscriminatorValue,
iterativeFunction: IterativeCollectCalendarContainers, iterativeFunction: IterativeCollectCalendarContainers,
}, },

View File

@ -7,6 +7,7 @@ import (
msfolderdelta "github.com/microsoftgraph/msgraph-sdk-go/users" msfolderdelta "github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
@ -31,22 +32,10 @@ type mailFolderCache struct {
func (mc *mailFolderCache) populateMailRoot( func (mc *mailFolderCache) populateMailRoot(
ctx context.Context, ctx context.Context,
) error { ) error {
wantedOpts := []string{"displayName", "parentFolderId"}
opts, err := optionsForMailFoldersItem(wantedOpts)
if err != nil {
return errors.Wrapf(err, "getting options for mail folders %v", wantedOpts)
}
for _, fldr := range []string{rootFolderAlias, DefaultMailFolder} { for _, fldr := range []string{rootFolderAlias, DefaultMailFolder} {
var directory string var directory string
f, err := mc. f, err := api.GetMailFolderByID(ctx, mc.gs, mc.userID, fldr, "displayName", "parentFolderId")
gs.
Client().
UsersById(mc.userID).
MailFoldersById(fldr).
Get(ctx, opts)
if err != nil { if err != nil {
return errors.Wrap(err, "fetching root folder"+support.ConnectorStackErrorTrace(err)) return errors.Wrap(err, "fetching root folder"+support.ConnectorStackErrorTrace(err))
} }
@ -56,7 +45,6 @@ func (mc *mailFolderCache) populateMailRoot(
} }
temp := graph.NewCacheFolder(f, path.Builder{}.Append(directory)) temp := graph.NewCacheFolder(f, path.Builder{}.Append(directory))
if err := mc.addFolder(temp); err != nil { if err := mc.addFolder(temp); err != nil {
return errors.Wrap(err, "initializing mail resolver") return errors.Wrap(err, "initializing mail resolver")
} }
@ -79,15 +67,7 @@ func (mc *mailFolderCache) Populate(
return err return err
} }
// Even though this uses the `Delta` query, we do no store or re-use query := api.GetAllMailFoldersBuilder(ctx, mc.gs, mc.userID)
// the delta-link tokens like with other queries. The goal is always
// to retrieve the complete history of folders.
query := mc.
gs.
Client().
UsersById(mc.userID).
MailFolders().
Delta()
var errs *multierror.Error var errs *multierror.Error

View File

@ -0,0 +1,269 @@
package exchange
import (
"context"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/path"
)
type ExchangeRestoreSuite struct {
suite.Suite
gs graph.Servicer
}
func TestExchangeRestoreSuite(t *testing.T) {
tester.RunOnAny(
t,
tester.CorsoCITests,
tester.CorsoConnectorRestoreExchangeCollectionTests)
suite.Run(t, new(ExchangeRestoreSuite))
}
func (suite *ExchangeRestoreSuite) SetupSuite() {
t := suite.T()
tester.MustGetEnvSets(t, tester.AWSStorageCredEnvs, tester.M365AcctCredEnvs)
a := tester.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err)
adpt, err := graph.CreateAdapter(
m365.AzureTenantID,
m365.AzureClientID,
m365.AzureClientSecret)
require.NoError(t, err)
suite.gs = graph.NewService(adpt)
require.NoError(suite.T(), err)
}
// TestRestoreContact ensures contact object can be created, placed into
// the Corso Folder. The function handles test clean-up.
func (suite *ExchangeRestoreSuite) TestRestoreContact() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
now = time.Now()
folderName = "TestRestoreContact: " + common.FormatSimpleDateTime(now)
)
aFolder, err := api.CreateContactFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
folderID := *aFolder.GetId()
defer func() {
// Remove the folder containing contact prior to exiting test
err = api.DeleteContactFolder(ctx, suite.gs, userID, folderID)
assert.NoError(t, err)
}()
info, err := RestoreExchangeContact(ctx,
mockconnector.GetMockContactBytes("Corso TestContact"),
suite.gs,
control.Copy,
folderID,
userID)
assert.NoError(t, err, support.ConnectorStackErrorTrace(err))
assert.NotNil(t, info, "contact item info")
}
// TestRestoreEvent verifies that event object is able to created
// and sent into the test account of the Corso user in the newly created Corso Calendar
func (suite *ExchangeRestoreSuite) TestRestoreEvent() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
userID = tester.M365UserID(t)
name = "TestRestoreEvent: " + common.FormatSimpleDateTime(time.Now())
)
calendar, err := api.CreateCalendar(ctx, suite.gs, userID, name)
require.NoError(t, err)
calendarID := *calendar.GetId()
defer func() {
// Removes calendar containing events created during the test
err = api.DeleteCalendar(ctx, suite.gs, userID, calendarID)
assert.NoError(t, err)
}()
info, err := RestoreExchangeEvent(ctx,
mockconnector.GetMockEventWithAttendeesBytes(name),
suite.gs,
control.Copy,
calendarID,
userID)
assert.NoError(t, err, support.ConnectorStackErrorTrace(err))
assert.NotNil(t, info, "event item info")
}
// TestRestoreExchangeObject verifies path.Category usage for restored objects
func (suite *ExchangeRestoreSuite) TestRestoreExchangeObject() {
a := tester.NewM365Account(suite.T())
m365, err := a.M365Config()
require.NoError(suite.T(), err)
service, err := createService(m365)
require.NoError(suite.T(), err)
userID := tester.M365UserID(suite.T())
now := time.Now()
tests := []struct {
name string
bytes []byte
category path.CategoryType
cleanupFunc func(context.Context, graph.Servicer, string, string) error
destination func(*testing.T, context.Context) string
}{
{
name: "Test Mail",
bytes: mockconnector.GetMockMessageBytes("Restore Exchange Object"),
category: path.EmailCategory,
cleanupFunc: api.DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailObject: " + common.FormatSimpleDateTime(now)
folder, err := api.CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: One Direct Attachment",
bytes: mockconnector.GetMockMessageWithDirectAttachment("Restore 1 Attachment"),
category: path.EmailCategory,
cleanupFunc: api.DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithAttachment: " + common.FormatSimpleDateTime(now)
folder, err := api.CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: One Large Attachment",
bytes: mockconnector.GetMockMessageWithLargeAttachment("Restore Large Attachment"),
category: path.EmailCategory,
cleanupFunc: api.DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithLargeAttachment: " + common.FormatSimpleDateTime(now)
folder, err := api.CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: Two Attachments",
bytes: mockconnector.GetMockMessageWithTwoAttachments("Restore 2 Attachments"),
category: path.EmailCategory,
cleanupFunc: api.DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithAttachments: " + common.FormatSimpleDateTime(now)
folder, err := api.CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Mail: Reference(OneDrive) Attachment",
bytes: mockconnector.GetMessageWithOneDriveAttachment("Restore Reference(OneDrive) Attachment"),
category: path.EmailCategory,
cleanupFunc: api.DeleteMailFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreMailwithReferenceAttachment: " + common.FormatSimpleDateTime(now)
folder, err := api.CreateMailFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
// TODO: #884 - reinstate when able to specify root folder by name
{
name: "Test Contact",
bytes: mockconnector.GetMockContactBytes("Test_Omega"),
category: path.ContactsCategory,
cleanupFunc: api.DeleteContactFolder,
destination: func(t *testing.T, ctx context.Context) string {
folderName := "TestRestoreContactObject: " + common.FormatSimpleDateTime(now)
folder, err := api.CreateContactFolder(ctx, suite.gs, userID, folderName)
require.NoError(t, err)
return *folder.GetId()
},
},
{
name: "Test Events",
bytes: mockconnector.GetDefaultMockEventBytes("Restored Event Object"),
category: path.EventsCategory,
cleanupFunc: api.DeleteCalendar,
destination: func(t *testing.T, ctx context.Context) string {
calendarName := "TestRestoreEventObject: " + common.FormatSimpleDateTime(now)
calendar, err := api.CreateCalendar(ctx, suite.gs, userID, calendarName)
require.NoError(t, err)
return *calendar.GetId()
},
},
{
name: "Test Event with Attachment",
bytes: mockconnector.GetMockEventWithAttachment("Restored Event Attachment"),
category: path.EventsCategory,
cleanupFunc: api.DeleteCalendar,
destination: func(t *testing.T, ctx context.Context) string {
calendarName := "TestRestoreEventObject_" + common.FormatSimpleDateTime(now)
calendar, err := api.CreateCalendar(ctx, suite.gs, userID, calendarName)
require.NoError(t, err)
return *calendar.GetId()
},
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
ctx, flush := tester.NewContext()
defer flush()
destination := test.destination(t, ctx)
info, err := RestoreExchangeObject(
ctx,
test.bytes,
test.category,
control.Copy,
service,
destination,
userID,
)
assert.NoError(t, err, support.ConnectorStackErrorTrace(err))
assert.NotNil(t, info, "item info is populated")
cleanupError := test.cleanupFunc(ctx, service, userID, destination)
assert.NoError(t, cleanupError)
})
}
}

View File

@ -4,7 +4,6 @@ import (
"context" "context"
"fmt" "fmt"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
@ -15,9 +14,6 @@ import (
var ErrFolderNotFound = errors.New("folder not found") var ErrFolderNotFound = errors.New("folder not found")
// 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) (*graph.Service, error) { func createService(credentials account.M365Config) (*graph.Service, error) {
adapter, err := graph.CreateAdapter( adapter, err := graph.CreateAdapter(
credentials.AzureTenantID, credentials.AzureTenantID,
@ -31,76 +27,7 @@ func createService(credentials account.M365Config) (*graph.Service, error) {
return graph.NewService(adapter), nil return graph.NewService(adapter), nil
} }
// CreateMailFolder makes a mail folder iff a folder of the same name does not exist // populateExchangeContainerResolver gets a folder resolver if one is available for
// Reference: https://docs.microsoft.com/en-us/graph/api/user-post-mailfolders?view=graph-rest-1.0&tabs=http
func CreateMailFolder(ctx context.Context, gs graph.Servicer, user, folder string) (models.MailFolderable, error) {
isHidden := false
requestBody := models.NewMailFolder()
requestBody.SetDisplayName(&folder)
requestBody.SetIsHidden(&isHidden)
return gs.Client().UsersById(user).MailFolders().Post(ctx, requestBody, nil)
}
func CreateMailFolderWithParent(
ctx context.Context,
gs graph.Servicer,
user, folder, parentID string,
) (models.MailFolderable, error) {
isHidden := false
requestBody := models.NewMailFolder()
requestBody.SetDisplayName(&folder)
requestBody.SetIsHidden(&isHidden)
return gs.Client().
UsersById(user).
MailFoldersById(parentID).
ChildFolders().
Post(ctx, requestBody, nil)
}
// DeleteMailFolder removes a mail folder with the corresponding M365 ID from the user's M365 Exchange account
// Reference: https://docs.microsoft.com/en-us/graph/api/mailfolder-delete?view=graph-rest-1.0&tabs=http
func DeleteMailFolder(ctx context.Context, gs graph.Servicer, user, folderID string) error {
return gs.Client().UsersById(user).MailFoldersById(folderID).Delete(ctx, nil)
}
// CreateCalendar makes an event Calendar with the name in the user's M365 exchange account
// Reference: https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=go
func CreateCalendar(ctx context.Context, gs graph.Servicer, user, calendarName string) (models.Calendarable, error) {
requestbody := models.NewCalendar()
requestbody.SetName(&calendarName)
return gs.Client().UsersById(user).Calendars().Post(ctx, requestbody, nil)
}
// DeleteCalendar removes calendar from user's M365 account
// Reference: https://docs.microsoft.com/en-us/graph/api/calendar-delete?view=graph-rest-1.0&tabs=go
func DeleteCalendar(ctx context.Context, gs graph.Servicer, user, calendarID string) error {
return gs.Client().UsersById(user).CalendarsById(calendarID).Delete(ctx, nil)
}
// CreateContactFolder makes a contact folder with the displayName of folderName.
// If successful, returns the created folder object.
func CreateContactFolder(
ctx context.Context,
gs graph.Servicer,
user, folderName string,
) (models.ContactFolderable, error) {
requestBody := models.NewContactFolder()
temp := folderName
requestBody.SetDisplayName(&temp)
return gs.Client().UsersById(user).ContactFolders().Post(ctx, requestBody, nil)
}
// DeleteContactFolder deletes the ContactFolder associated with the M365 ID if permissions are valid.
// Errors returned if the function call was not successful.
func DeleteContactFolder(ctx context.Context, gs graph.Servicer, user, folderID string) error {
return gs.Client().UsersById(user).ContactFoldersById(folderID).Delete(ctx, nil)
}
// PopulateExchangeContainerResolver gets a folder resolver if one is available for
// this category of data. If one is not available, returns nil so that other // this category of data. If one is not available, returns nil so that other
// logic in the caller can complete as long as they check if the resolver is not // logic in the caller can complete as long as they check if the resolver is not
// nil. If an error occurs populating the resolver, returns an error. // nil. If an error occurs populating the resolver, returns an error.

View File

@ -5,11 +5,10 @@ import (
"fmt" "fmt"
"strings" "strings"
multierror "github.com/hashicorp/go-multierror"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
msuser "github.com/microsoftgraph/msgraph-sdk-go/users"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -19,14 +18,6 @@ import (
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
// carries details about delta retrieval in aggregators
type deltaUpdate struct {
// the deltaLink itself
url string
// true if the old delta was marked as invalid
reset bool
}
// filterContainersAndFillCollections is a utility function // filterContainersAndFillCollections is a utility function
// that places the M365 object ids belonging to specific directories // that places the M365 object ids belonging to specific directories
// into a Collection. Messages outside of those directories are omitted. // into a Collection. Messages outside of those directories are omitted.
@ -104,14 +95,14 @@ func filterContainersAndFillCollections(
// to reset which will prevent any old items from being retained in // to reset which will prevent any old items from being retained in
// storage. If the container (or its children) are sill missing // storage. If the container (or its children) are sill missing
// on the next backup, they'll get tombstoned. // on the next backup, they'll get tombstoned.
newDelta = deltaUpdate{reset: true} newDelta = api.DeltaUpdate{Reset: true}
} }
continue continue
} }
if len(newDelta.url) > 0 { if len(newDelta.URL) > 0 {
deltaURLs[cID] = newDelta.url deltaURLs[cID] = newDelta.URL
} }
edc := NewCollection( edc := NewCollection(
@ -122,7 +113,7 @@ func filterContainersAndFillCollections(
service, service,
statusUpdater, statusUpdater,
ctrlOpts, ctrlOpts,
newDelta.reset, newDelta.Reset,
) )
collections[cID] = &edc collections[cID] = &edc
@ -278,282 +269,17 @@ type FetchIDFunc func(
ctx context.Context, ctx context.Context,
gs graph.Servicer, gs graph.Servicer,
user, containerID, oldDeltaToken string, user, containerID, oldDeltaToken string,
) ([]string, []string, deltaUpdate, error) ) ([]string, []string, api.DeltaUpdate, error)
func getFetchIDFunc(category path.CategoryType) (FetchIDFunc, error) { func getFetchIDFunc(category path.CategoryType) (FetchIDFunc, error) {
switch category { switch category {
case path.EmailCategory: case path.EmailCategory:
return FetchMessageIDsFromDirectory, nil return api.FetchMessageIDsFromDirectory, nil
case path.EventsCategory: case path.EventsCategory:
return FetchEventIDsFromCalendar, nil return api.FetchEventIDsFromCalendar, nil
case path.ContactsCategory: case path.ContactsCategory:
return FetchContactIDsFromDirectory, nil return api.FetchContactIDsFromDirectory, nil
default: default:
return nil, fmt.Errorf("category %s not supported by getFetchIDFunc", category) return nil, fmt.Errorf("category %s not supported by getFetchIDFunc", category)
} }
} }
// ---------------------------------------------------------------------------
// events
// ---------------------------------------------------------------------------
// FetchEventIDsFromCalendar returns a list of all M365IDs of events of the targeted Calendar.
func FetchEventIDsFromCalendar(
ctx context.Context,
gs graph.Servicer,
user, calendarID, oldDelta string,
) ([]string, []string, deltaUpdate, error) {
var (
errs *multierror.Error
ids []string
)
options, err := optionsForEventsByCalendar([]string{"id"})
if err != nil {
return nil, nil, deltaUpdate{}, err
}
builder := gs.Client().
UsersById(user).
CalendarsById(calendarID).
Events()
for {
resp, err := builder.Get(ctx, options)
if err != nil {
if err := graph.IsErrDeletedInFlight(err); err != nil {
return nil, nil, deltaUpdate{}, err
}
return nil, nil, deltaUpdate{}, errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
for _, item := range resp.GetValue() {
if item.GetId() == nil {
errs = multierror.Append(
errs,
errors.Errorf("event with nil ID in calendar %s", calendarID),
)
// TODO(ashmrtn): Handle fail-fast.
continue
}
ids = append(ids, *item.GetId())
}
nextLink := resp.GetOdataNextLink()
if nextLink == nil || len(*nextLink) == 0 {
break
}
builder = msuser.NewItemCalendarsItemEventsRequestBuilder(*nextLink, gs.Adapter())
}
// Events don't have a delta endpoint so just return an empty string.
return ids, nil, deltaUpdate{}, errs.ErrorOrNil()
}
// ---------------------------------------------------------------------------
// contacts
// ---------------------------------------------------------------------------
// FetchContactIDsFromDirectory function that returns a list of all the m365IDs of the contacts
// of the targeted directory
func FetchContactIDsFromDirectory(
ctx context.Context,
gs graph.Servicer,
user, directoryID, oldDelta string,
) ([]string, []string, deltaUpdate, error) {
var (
errs *multierror.Error
ids []string
removedIDs []string
deltaURL string
resetDelta bool
)
options, err := optionsForContactFoldersItemDelta([]string{"parentFolderId"})
if err != nil {
return nil, nil, deltaUpdate{}, errors.Wrap(err, "getting query options")
}
getIDs := func(builder *msuser.ItemContactFoldersItemContactsDeltaRequestBuilder) error {
for {
resp, err := builder.Get(ctx, options)
if err != nil {
if err := graph.IsErrDeletedInFlight(err); err != nil {
return err
}
if err := graph.IsErrInvalidDelta(err); err != nil {
return err
}
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
for _, item := range resp.GetValue() {
if item.GetId() == nil {
errs = multierror.Append(
errs,
errors.Errorf("item with nil ID in folder %s", directoryID),
)
// TODO(ashmrtn): Handle fail-fast.
continue
}
if item.GetAdditionalData()[graph.AddtlDataRemoved] == nil {
ids = append(ids, *item.GetId())
} else {
removedIDs = append(removedIDs, *item.GetId())
}
}
delta := resp.GetOdataDeltaLink()
if delta != nil && len(*delta) > 0 {
deltaURL = *delta
}
nextLink := resp.GetOdataNextLink()
if nextLink == nil || len(*nextLink) == 0 {
break
}
builder = msuser.NewItemContactFoldersItemContactsDeltaRequestBuilder(*nextLink, gs.Adapter())
}
return nil
}
if len(oldDelta) > 0 {
err := getIDs(msuser.NewItemContactFoldersItemContactsDeltaRequestBuilder(oldDelta, gs.Adapter()))
// happy path
if err == nil {
return ids, removedIDs, deltaUpdate{deltaURL, false}, errs.ErrorOrNil()
}
// only return on error if it is NOT a delta issue.
// otherwise we'll retry the call with the regular builder
if graph.IsErrInvalidDelta(err) == nil {
return nil, nil, deltaUpdate{}, err
}
resetDelta = true
errs = nil
}
builder := gs.Client().
UsersById(user).
ContactFoldersById(directoryID).
Contacts().
Delta()
if err := getIDs(builder); err != nil {
return nil, nil, deltaUpdate{}, err
}
return ids, removedIDs, deltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil()
}
// ---------------------------------------------------------------------------
// messages
// ---------------------------------------------------------------------------
// FetchMessageIDsFromDirectory function that returns a list of all the m365IDs of the exchange.Mail
// of the targeted directory
func FetchMessageIDsFromDirectory(
ctx context.Context,
gs graph.Servicer,
user, directoryID, oldDelta string,
) ([]string, []string, deltaUpdate, error) {
var (
errs *multierror.Error
ids []string
removedIDs []string
deltaURL string
resetDelta bool
)
options, err := optionsForFolderMessagesDelta([]string{"isRead"})
if err != nil {
return nil, nil, deltaUpdate{}, errors.Wrap(err, "getting query options")
}
getIDs := func(builder *msuser.ItemMailFoldersItemMessagesDeltaRequestBuilder) error {
for {
resp, err := builder.Get(ctx, options)
if err != nil {
if err := graph.IsErrDeletedInFlight(err); err != nil {
return err
}
if err := graph.IsErrInvalidDelta(err); err != nil {
return err
}
return errors.Wrap(err, support.ConnectorStackErrorTrace(err))
}
for _, item := range resp.GetValue() {
if item.GetId() == nil {
errs = multierror.Append(
errs,
errors.Errorf("item with nil ID in folder %s", directoryID),
)
// TODO(ashmrtn): Handle fail-fast.
continue
}
if item.GetAdditionalData()[graph.AddtlDataRemoved] == nil {
ids = append(ids, *item.GetId())
} else {
removedIDs = append(removedIDs, *item.GetId())
}
}
delta := resp.GetOdataDeltaLink()
if delta != nil && len(*delta) > 0 {
deltaURL = *delta
}
nextLink := resp.GetOdataNextLink()
if nextLink == nil || len(*nextLink) == 0 {
break
}
builder = msuser.NewItemMailFoldersItemMessagesDeltaRequestBuilder(*nextLink, gs.Adapter())
}
return nil
}
if len(oldDelta) > 0 {
err := getIDs(msuser.NewItemMailFoldersItemMessagesDeltaRequestBuilder(oldDelta, gs.Adapter()))
// happy path
if err == nil {
return ids, removedIDs, deltaUpdate{deltaURL, false}, errs.ErrorOrNil()
}
// only return on error if it is NOT a delta issue.
// otherwise we'll retry the call with the regular builder
if graph.IsErrInvalidDelta(err) == nil {
return nil, nil, deltaUpdate{}, err
}
resetDelta = true
errs = nil
}
builder := gs.Client().
UsersById(user).
MailFoldersById(directoryID).
Messages().
Delta()
if err := getIDs(builder); err != nil {
return nil, nil, deltaUpdate{}, err
}
return ids, removedIDs, deltaUpdate{deltaURL, resetDelta}, errs.ErrorOrNil()
}

View File

@ -1,109 +0,0 @@
package exchange
import (
"context"
absser "github.com/microsoft/kiota-abstractions-go/serialization"
"github.com/alcionai/corso/src/internal/connector/graph"
)
// GraphQuery represents functions which perform exchange-specific queries
// into M365 backstore. Responses -> returned items will only contain the information
// that is included in the options
// TODO: use selector or path for granularity into specific folders or specific date ranges
type GraphQuery func(ctx context.Context, gs graph.Servicer, userID string) (absser.Parsable, error)
// GetAllContactsForUser is a GraphQuery function for querying all the contacts in a user's account
func GetAllContactsForUser(ctx context.Context, gs graph.Servicer, user string) (absser.Parsable, error) {
selecting := []string{"parentFolderId"}
options, err := optionsForContacts(selecting)
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).Contacts().Get(ctx, options)
}
// 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(ctx context.Context, gs graph.Servicer, user string) (absser.Parsable, error) {
options, err := optionsForMailFolders([]string{"displayName"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).MailFolders().Get(ctx, options)
}
func GetAllCalendarNamesForUser(ctx context.Context, gs graph.Servicer, user string) (absser.Parsable, error) {
options, err := optionsForCalendars([]string{"name", "owner"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).Calendars().Get(ctx, options)
}
// GetDefaultContactFolderForUser is a GraphQuery function for getting the ContactFolderId
// and display names for the default "Contacts" folder.
// Only returns the default Contact Folder
func GetDefaultContactFolderForUser(ctx context.Context, gs graph.Servicer, user string) (absser.Parsable, error) {
options, err := optionsForContactChildFolders([]string{"displayName", "parentFolderId"})
if err != nil {
return nil, err
}
return gs.Client().
UsersById(user).
ContactFoldersById(rootFolderAlias).
ChildFolders().
Get(ctx, options)
}
// GetAllContactFolderNamesForUser is a GraphQuery function for getting ContactFolderId
// and display names for contacts. All other information is omitted.
// Does not return the default Contact Folder
func GetAllContactFolderNamesForUser(ctx context.Context, gs graph.Servicer, user string) (absser.Parsable, error) {
options, err := optionsForContactFolders([]string{"displayName", "parentFolderId"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).ContactFolders().Get(ctx, options)
}
// GetAllEvents for User. Default returns EventResponseCollection for future events.
// of the time that the call was made. 'calendar' option must be present to gain
// access to additional data map in future calls.
func GetAllEventsForUser(ctx context.Context, gs graph.Servicer, user string) (absser.Parsable, error) {
options, err := optionsForEvents([]string{"id", "calendar"})
if err != nil {
return nil, err
}
return gs.Client().UsersById(user).Events().Get(ctx, options)
}
// GraphRetrievalFunctions are functions from the Microsoft Graph API that retrieve
// the default associated data of a M365 object. This varies by object. Additional
// Queries must be run to obtain the omitted fields.
type GraphRetrievalFunc func(ctx context.Context, gs graph.Servicer, user, m365ID string) (absser.Parsable, error)
// RetrieveContactDataForUser is a GraphRetrievalFun that returns all associated fields.
func RetrieveContactDataForUser(ctx context.Context, gs graph.Servicer, user, m365ID string) (absser.Parsable, error) {
return gs.Client().UsersById(user).ContactsById(m365ID).Get(ctx, nil)
}
// RetrieveEventDataForUser is a GraphRetrievalFunc that returns event data.
// Calendarable and attachment fields are omitted due to size
func RetrieveEventDataForUser(ctx context.Context, gs graph.Servicer, user, m365ID string) (absser.Parsable, error) {
return gs.Client().UsersById(user).EventsById(m365ID).Get(ctx, nil)
}
// RetrieveMessageDataForUser is a GraphRetrievalFunc that returns message data.
// Attachment field is omitted due to size.
func RetrieveMessageDataForUser(ctx context.Context, gs graph.Servicer, user, m365ID string) (absser.Parsable, error) {
return gs.Client().UsersById(user).MessagesById(m365ID).Get(ctx, nil)
}

View File

@ -11,6 +11,7 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
@ -532,8 +533,12 @@ func establishMailRestoreLocation(
continue continue
} }
temp, err := CreateMailFolderWithParent(ctx, temp, err := api.CreateMailFolderWithParent(
service, user, folder, folderID) ctx,
service,
user,
folder,
folderID)
if err != nil { if err != nil {
// Should only error if cache malfunctions or incorrect parameters // Should only error if cache malfunctions or incorrect parameters
return "", errors.Wrap(err, support.ConnectorStackErrorTrace(err)) return "", errors.Wrap(err, support.ConnectorStackErrorTrace(err))
@ -580,7 +585,7 @@ func establishContactsRestoreLocation(
return cached, nil return cached, nil
} }
temp, err := CreateContactFolder(ctx, gs, user, folders[0]) temp, err := api.CreateContactFolder(ctx, gs, user, folders[0])
if err != nil { if err != nil {
return "", errors.Wrap(err, support.ConnectorStackErrorTrace(err)) return "", errors.Wrap(err, support.ConnectorStackErrorTrace(err))
} }
@ -613,7 +618,7 @@ func establishEventsRestoreLocation(
return cached, nil return cached, nil
} }
temp, err := CreateCalendar(ctx, gs, user, folders[0]) temp, err := api.CreateCalendar(ctx, gs, user, folders[0])
if err != nil { if err != nil {
return "", errors.Wrap(err, support.ConnectorStackErrorTrace(err)) return "", errors.Wrap(err, support.ConnectorStackErrorTrace(err))
} }

View File

@ -17,6 +17,7 @@ import (
"github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector" "github.com/alcionai/corso/src/internal/connector"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/exchange/api"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/mockconnector" "github.com/alcionai/corso/src/internal/connector/mockconnector"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
@ -924,7 +925,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
switch category { switch category {
case path.EmailCategory: case path.EmailCategory:
ids, _, _, err := exchange.FetchMessageIDsFromDirectory(ctx, gc.Service, suite.user, containerID, "") ids, _, _, err := api.FetchMessageIDsFromDirectory(ctx, gc.Service, suite.user, containerID, "")
require.NoError(t, err, "getting message ids") require.NoError(t, err, "getting message ids")
require.NotEmpty(t, ids, "message ids in folder") require.NotEmpty(t, ids, "message ids in folder")
@ -932,7 +933,7 @@ func (suite *BackupOpIntegrationSuite) TestBackup_Run_exchangeIncrementals() {
require.NoError(t, err, "deleting email item: %s", support.ConnectorStackErrorTrace(err)) require.NoError(t, err, "deleting email item: %s", support.ConnectorStackErrorTrace(err))
case path.ContactsCategory: case path.ContactsCategory:
ids, _, _, err := exchange.FetchContactIDsFromDirectory(ctx, gc.Service, suite.user, containerID, "") ids, _, _, err := api.FetchContactIDsFromDirectory(ctx, gc.Service, suite.user, containerID, "")
require.NoError(t, err, "getting contact ids") require.NoError(t, err, "getting contact ids")
require.NotEmpty(t, ids, "contact ids in folder") require.NotEmpty(t, ids, "contact ids in folder")

View File

@ -22,6 +22,8 @@ const (
CorsoConnectorCreateExchangeCollectionTests = "CORSO_CONNECTOR_CREATE_EXCHANGE_COLLECTION_TESTS" CorsoConnectorCreateExchangeCollectionTests = "CORSO_CONNECTOR_CREATE_EXCHANGE_COLLECTION_TESTS"
CorsoConnectorCreateSharePointCollectionTests = "CORSO_CONNECTOR_CREATE_SHAREPOINT_COLLECTION_TESTS" CorsoConnectorCreateSharePointCollectionTests = "CORSO_CONNECTOR_CREATE_SHAREPOINT_COLLECTION_TESTS"
CorsoConnectorDataCollectionTests = "CORSO_CONNECTOR_DATA_COLLECTION_TESTS" CorsoConnectorDataCollectionTests = "CORSO_CONNECTOR_DATA_COLLECTION_TESTS"
CorsoConnectorExchangeFolderCacheTests = "CORSO_CONNECTOR_EXCHANGE_FOLDER_CACHE_TESTS"
CorsoConnectorRestoreExchangeCollectionTests = "CORSO_CONNECTOR_RESTORE_EXCHANGE_COLLECTION_TESTS"
CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS" CorsoGraphConnectorTests = "CORSO_GRAPH_CONNECTOR_TESTS"
CorsoGraphConnectorExchangeTests = "CORSO_GRAPH_CONNECTOR_EXCHANGE_TESTS" CorsoGraphConnectorExchangeTests = "CORSO_GRAPH_CONNECTOR_EXCHANGE_TESTS"
CorsoGraphConnectorOneDriveTests = "CORSO_GRAPH_CONNECTOR_ONE_DRIVE_TESTS" CorsoGraphConnectorOneDriveTests = "CORSO_GRAPH_CONNECTOR_ONE_DRIVE_TESTS"