ensure od and exchange can restore in-place (#3703)

Adds changes (mostly to exchange) that allow in- place item restoration.  Onedrive basically already supported in-place restores, however we weren't attempting them.  Exchange needed full support of GetContainerByName across its categories.

Next steps: add CLI integration to simplify manual testing.  Then add automation testing to exercise all in-place restore conditions and configurations.

---

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

- [x] 🕐 Yes, but in a later PR

#### Type of change

- [x] 🌻 Feature

#### Issue(s)

* #3562

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-07-05 15:44:55 -06:00 committed by GitHub
parent 6fff3f7d1d
commit 8cc3069a6a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 486 additions and 87 deletions

View File

@ -47,13 +47,16 @@ func (h contactRestoreHandler) formatRestoreDestination(
func (h contactRestoreHandler) CreateContainer( func (h contactRestoreHandler) CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName, _ string, // parent container not used userID, _, containerName string, // parent container not used
) (graph.Container, error) { ) (graph.Container, error) {
return h.ac.CreateContainer(ctx, userID, containerName, "") return h.ac.CreateContainer(ctx, userID, "", containerName)
} }
func (h contactRestoreHandler) containerSearcher() containerByNamer { func (h contactRestoreHandler) GetContainerByName(
return nil ctx context.Context,
userID, _, containerName string, // parent container not used
) (graph.Container, error) {
return h.ac.GetContainerByName(ctx, userID, "", containerName)
} }
// always returns the provided value // always returns the provided value

View File

@ -48,13 +48,16 @@ func (h eventRestoreHandler) formatRestoreDestination(
func (h eventRestoreHandler) CreateContainer( func (h eventRestoreHandler) CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName, _ string, // parent container not used userID, _, containerName string, // parent container not used
) (graph.Container, error) { ) (graph.Container, error) {
return h.ac.CreateContainer(ctx, userID, containerName, "") return h.ac.CreateContainer(ctx, userID, "", containerName)
} }
func (h eventRestoreHandler) containerSearcher() containerByNamer { func (h eventRestoreHandler) GetContainerByName(
return h.ac ctx context.Context,
userID, _, containerName string, // parent container not used
) (graph.Container, error) {
return h.ac.GetContainerByName(ctx, userID, "", containerName)
} }
// always returns the provided value // always returns the provided value

View File

@ -86,19 +86,14 @@ type itemRestorer interface {
// produces structs that interface with the graph/cache_container // produces structs that interface with the graph/cache_container
// CachedContainer interface. // CachedContainer interface.
type containerAPI interface { type containerAPI interface {
containerByNamer
// POSTs the creation of a new container // POSTs the creation of a new container
CreateContainer( CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName, parentContainerID string, userID, parentContainerID, containerName string,
) (graph.Container, error) ) (graph.Container, error)
// GETs a container by name.
// if containerByNamer is nil, this functionality is not supported
// and should be skipped by the caller.
// normally, we'd alias the func directly. The indirection here
// is because not all types comply with GetContainerByName.
containerSearcher() containerByNamer
// returns either the provided value (assumed to be the root // returns either the provided value (assumed to be the root
// folder for that cache tree), or the default root container // folder for that cache tree), or the default root container
// (if the category uses a root folder that exists above the // (if the category uses a root folder that exists above the
@ -110,7 +105,7 @@ type containerByNamer interface {
// searches for a container by name. // searches for a container by name.
GetContainerByName( GetContainerByName(
ctx context.Context, ctx context.Context,
userID, containerName string, userID, parentContainerID, containerName string,
) (graph.Container, error) ) (graph.Container, error)
} }

View File

@ -48,17 +48,20 @@ func (h mailRestoreHandler) formatRestoreDestination(
func (h mailRestoreHandler) CreateContainer( func (h mailRestoreHandler) CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName, parentContainerID string, userID, parentContainerID, containerName string,
) (graph.Container, error) { ) (graph.Container, error) {
if len(parentContainerID) == 0 { if len(parentContainerID) == 0 {
parentContainerID = rootFolderAlias parentContainerID = rootFolderAlias
} }
return h.ac.CreateContainer(ctx, userID, containerName, parentContainerID) return h.ac.CreateContainer(ctx, userID, parentContainerID, containerName)
} }
func (h mailRestoreHandler) containerSearcher() containerByNamer { func (h mailRestoreHandler) GetContainerByName(
return nil ctx context.Context,
userID, parentContainerID, containerName string,
) (graph.Container, error) {
return h.ac.GetContainerByName(ctx, userID, parentContainerID, containerName)
} }
// always returns rootFolderAlias // always returns rootFolderAlias

View File

@ -44,6 +44,7 @@ func ConsumeRestoreCollections(
el = errs.Local() el = errs.Local()
) )
// FIXME: should be user name
ctx = clues.Add(ctx, "resource_owner", clues.Hide(userID)) ctx = clues.Add(ctx, "resource_owner", clues.Hide(userID))
for _, dc := range dcs { for _, dc := range dcs {
@ -289,7 +290,7 @@ func getOrPopulateContainer(
return cached, nil return cached, nil
} }
c, err := ca.CreateContainer(ctx, userID, containerName, containerParentID) c, err := ca.CreateContainer(ctx, userID, containerParentID, containerName)
// 409 handling case: // 409 handling case:
// attempt to fetch the container by name and add that result to the cache. // attempt to fetch the container by name and add that result to the cache.
@ -297,11 +298,12 @@ func getOrPopulateContainer(
// sometimes the backend will create the folder despite the 5xx response, // sometimes the backend will create the folder despite the 5xx response,
// leaving our local containerResolver with inconsistent state. // leaving our local containerResolver with inconsistent state.
if graph.IsErrFolderExists(err) { if graph.IsErrFolderExists(err) {
cs := ca.containerSearcher() cc, e := ca.GetContainerByName(ctx, userID, containerParentID, containerName)
if cs != nil { if e != nil {
cc, e := cs.GetContainerByName(ctx, userID, containerName)
c = cc
err = clues.Stack(err, e) err = clues.Stack(err, e)
} else {
c = cc
err = nil
} }
} }

View File

@ -60,7 +60,7 @@ func (suite *RestoreIntgSuite) TestRestoreContact() {
handler = newContactRestoreHandler(suite.ac) handler = newContactRestoreHandler(suite.ac)
) )
aFolder, err := handler.ac.CreateContainer(ctx, userID, folderName, "") aFolder, err := handler.ac.CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
folderID := ptr.Val(aFolder.GetId()) folderID := ptr.Val(aFolder.GetId())
@ -96,7 +96,7 @@ func (suite *RestoreIntgSuite) TestRestoreEvent() {
handler = newEventRestoreHandler(suite.ac) handler = newEventRestoreHandler(suite.ac)
) )
calendar, err := handler.ac.CreateContainer(ctx, userID, subject, "") calendar, err := handler.ac.CreateContainer(ctx, userID, "", subject)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
calendarID := ptr.Val(calendar.GetId()) calendarID := ptr.Val(calendar.GetId())
@ -179,7 +179,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailobj").Location folderName := testdata.DefaultRestoreConfig("mailobj").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -192,7 +192,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailwattch").Location folderName := testdata.DefaultRestoreConfig("mailwattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -205,7 +205,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("eventwattch").Location folderName := testdata.DefaultRestoreConfig("eventwattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -218,7 +218,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailitemattch").Location folderName := testdata.DefaultRestoreConfig("mailitemattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -234,7 +234,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailbasicattch").Location folderName := testdata.DefaultRestoreConfig("mailbasicattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -250,7 +250,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailnestattch").Location folderName := testdata.DefaultRestoreConfig("mailnestattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -266,7 +266,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailcontactattch").Location folderName := testdata.DefaultRestoreConfig("mailcontactattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -279,7 +279,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("nestedattch").Location folderName := testdata.DefaultRestoreConfig("nestedattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -292,7 +292,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("maillargeattch").Location folderName := testdata.DefaultRestoreConfig("maillargeattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -305,7 +305,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailtwoattch").Location folderName := testdata.DefaultRestoreConfig("mailtwoattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -318,7 +318,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("mailrefattch").Location folderName := testdata.DefaultRestoreConfig("mailrefattch").Location
folder, err := handlers[path.EmailCategory]. folder, err := handlers[path.EmailCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -331,7 +331,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("contact").Location folderName := testdata.DefaultRestoreConfig("contact").Location
folder, err := handlers[path.ContactsCategory]. folder, err := handlers[path.ContactsCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(folder.GetId()) return ptr.Val(folder.GetId())
@ -344,7 +344,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("event").Location folderName := testdata.DefaultRestoreConfig("event").Location
calendar, err := handlers[path.EventsCategory]. calendar, err := handlers[path.EventsCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(calendar.GetId()) return ptr.Val(calendar.GetId())
@ -357,7 +357,7 @@ func (suite *RestoreIntgSuite) TestRestoreExchangeObject() {
destination: func(t *testing.T, ctx context.Context) string { destination: func(t *testing.T, ctx context.Context) string {
folderName := testdata.DefaultRestoreConfig("eventobj").Location folderName := testdata.DefaultRestoreConfig("eventobj").Location
calendar, err := handlers[path.EventsCategory]. calendar, err := handlers[path.EventsCategory].
CreateContainer(ctx, userID, folderName, "") CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
return ptr.Val(calendar.GetId()) return ptr.Val(calendar.GetId())
@ -400,7 +400,7 @@ func (suite *RestoreIntgSuite) TestRestoreAndBackupEvent_recurringInstancesWithA
handler = newEventRestoreHandler(suite.ac) handler = newEventRestoreHandler(suite.ac)
) )
calendar, err := handler.ac.CreateContainer(ctx, userID, subject, "") calendar, err := handler.ac.CreateContainer(ctx, userID, "", subject)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
calendarID := ptr.Val(calendar.GetId()) calendarID := ptr.Val(calendar.GetId())

View File

@ -37,8 +37,8 @@ type Contacts struct {
// If successful, returns the created folder object. // If successful, returns the created folder object.
func (c Contacts) CreateContainer( func (c Contacts) CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName string, // parentContainerID needed for iface, doesn't apply to contacts
_ string, // parentContainerID needed for iface, doesn't apply to contacts userID, _, containerName string,
) (graph.Container, error) { ) (graph.Container, error) {
body := models.NewContactFolder() body := models.NewContactFolder()
body.SetDisplayName(ptr.To(containerName)) body.SetDisplayName(ptr.To(containerName))
@ -117,6 +117,56 @@ func (c Contacts) GetContainerByID(
return c.GetFolder(ctx, userID, containerID) return c.GetFolder(ctx, userID, containerID)
} }
// GetContainerByName fetches a folder by name
func (c Contacts) GetContainerByName(
ctx context.Context,
// parentContainerID needed for iface, doesn't apply to contacts
userID, _, containerName string,
) (graph.Container, error) {
filter := fmt.Sprintf("displayName eq '%s'", containerName)
options := &users.ItemContactFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemContactFoldersRequestBuilderGetQueryParameters{
Filter: &filter,
},
}
ctx = clues.Add(ctx, "container_name", containerName)
resp, err := c.Stable.
Client().
Users().
ByUserId(userID).
ContactFolders().
Get(ctx, options)
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
gv := resp.GetValue()
if len(gv) == 0 {
return nil, clues.New("container not found").WithClues(ctx)
}
// We only allow the api to match one container with the provided name.
// Return an error if multiple container exist (unlikely) or if no container
// is found.
if len(gv) != 1 {
return nil, clues.New("unexpected number of folders returned").
With("returned_container_count", len(gv)).
WithClues(ctx)
}
// Sanity check ID and name
container := gv[0]
if err := graph.CheckIDAndName(container); err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return container, nil
}
func (c Contacts) PatchFolder( func (c Contacts) PatchFolder(
ctx context.Context, ctx context.Context,
userID, containerID string, userID, containerID string,

View File

@ -1,4 +1,4 @@
package api package api_test
import ( import (
"testing" "testing"
@ -7,11 +7,15 @@ import (
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock" exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control/testdata"
"github.com/alcionai/corso/src/pkg/services/m365/api"
) )
type ContactsAPIUnitSuite struct { type ContactsAPIUnitSuite struct {
@ -64,7 +68,7 @@ func (suite *ContactsAPIUnitSuite) TestContactInfo() {
for _, test := range tests { for _, test := range tests {
suite.Run(test.name, func() { suite.Run(test.name, func() {
contact, expected := test.contactAndRP() contact, expected := test.contactAndRP()
assert.Equal(suite.T(), expected, ContactInfo(contact)) assert.Equal(suite.T(), expected, api.ContactInfo(contact))
}) })
} }
} }
@ -99,9 +103,72 @@ func (suite *ContactsAPIUnitSuite) TestBytesToContactable() {
suite.Run(test.name, func() { suite.Run(test.name, func() {
t := suite.T() t := suite.T()
result, err := BytesToContactable(test.byteArray) result, err := api.BytesToContactable(test.byteArray)
test.checkError(t, err, clues.ToCore(err)) test.checkError(t, err, clues.ToCore(err))
test.isNil(t, result) test.isNil(t, result)
}) })
} }
} }
type ContactsAPIIntgSuite struct {
tester.Suite
its intgTesterSetup
}
func TestContactsAPIntgSuite(t *testing.T) {
suite.Run(t, &ContactsAPIIntgSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tester.M365AcctCredEnvs}),
})
}
func (suite *ContactsAPIIntgSuite) SetupSuite() {
suite.its = newIntegrationTesterSetup(suite.T())
}
func (suite *ContactsAPIIntgSuite) TestContacts_GetContainerByName() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
// contacts cannot filter for the parent "contacts" folder, so we
// have to hack this by creating a folder to match beforehand.
rc := testdata.DefaultRestoreConfig("contacts_api")
cc, err := suite.its.ac.Contacts().CreateContainer(
ctx,
suite.its.userID,
"",
rc.Location)
require.NoError(t, err, clues.ToCore(err))
table := []struct {
name string
expectErr assert.ErrorAssertionFunc
}{
{
name: ptr.Val(cc.GetDisplayName()),
expectErr: assert.NoError,
},
{
name: "smarfs",
expectErr: assert.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
_, err := suite.its.ac.
Contacts().
GetContainerByName(ctx, suite.its.userID, "", test.name)
test.expectErr(t, err, clues.ToCore(err))
})
}
}

View File

@ -21,6 +21,7 @@ import (
"github.com/alcionai/corso/src/internal/m365/graph" "github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
) )
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -44,8 +45,8 @@ type Events struct {
// Reference: https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=go // Reference: https://docs.microsoft.com/en-us/graph/api/user-post-calendars?view=graph-rest-1.0&tabs=go
func (c Events) CreateContainer( func (c Events) CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName string, // parentContainerID needed for iface, doesn't apply to events
_ string, // parentContainerID needed for iface, doesn't apply to contacts userID, _, containerName string,
) (graph.Container, error) { ) (graph.Container, error) {
body := models.NewCalendar() body := models.NewCalendar()
body.SetName(&containerName) body.SetName(&containerName)
@ -132,7 +133,8 @@ func (c Events) GetContainerByID(
// GetContainerByName fetches a calendar by name // GetContainerByName fetches a calendar by name
func (c Events) GetContainerByName( func (c Events) GetContainerByName(
ctx context.Context, ctx context.Context,
userID, containerName string, // parentContainerID needed for iface, doesn't apply to events
userID, _, containerName string,
) (graph.Container, error) { ) (graph.Container, error) {
filter := fmt.Sprintf("name eq '%s'", containerName) filter := fmt.Sprintf("name eq '%s'", containerName)
options := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{ options := &users.ItemCalendarsRequestBuilderGetRequestConfiguration{
@ -141,7 +143,7 @@ func (c Events) GetContainerByName(
}, },
} }
ctx = clues.Add(ctx, "calendar_name", containerName) ctx = clues.Add(ctx, "container_name", containerName)
resp, err := c.Stable. resp, err := c.Stable.
Client(). Client().
@ -153,24 +155,25 @@ func (c Events) GetContainerByName(
return nil, graph.Stack(ctx, err).WithClues(ctx) return nil, graph.Stack(ctx, err).WithClues(ctx)
} }
// We only allow the api to match one calendar with provided name. gv := resp.GetValue()
// Return an error if multiple calendars exist (unlikely) or if no calendar
// is found. if len(gv) == 0 {
if len(resp.GetValue()) != 1 { return nil, clues.New("container not found").WithClues(ctx)
err = clues.New("unexpected number of calendars returned").
With("returned_calendar_count", len(resp.GetValue()))
return nil, err
} }
// We only allow the api to match one calendar with the provided name.
// If we match multiples, we'll eagerly return the first one.
logger.Ctx(ctx).Debugw("calendars matched the name search", "calendar_count", len(gv))
// Sanity check ID and name // Sanity check ID and name
cal := resp.GetValue()[0] cal := gv[0]
cd := CalendarDisplayable{Calendarable: cal} container := graph.CalendarDisplayable{Calendarable: cal}
if err := graph.CheckIDAndName(cd); err != nil { if err := graph.CheckIDAndName(container); err != nil {
return nil, err return nil, clues.Stack(err).WithClues(ctx)
} }
return graph.CalendarDisplayable{Calendarable: cal}, nil return container, nil
} }
func (c Events) PatchCalendar( func (c Events) PatchCalendar(

View File

@ -5,6 +5,7 @@ import (
"time" "time"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/h2non/gock"
"github.com/microsoftgraph/msgraph-sdk-go/models" "github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -235,7 +236,7 @@ func (suite *EventsAPIIntgSuite) SetupSuite() {
suite.its = newIntegrationTesterSetup(suite.T()) suite.its = newIntegrationTesterSetup(suite.T())
} }
func (suite *EventsAPIIntgSuite) TestRestoreLargeAttachment() { func (suite *EventsAPIIntgSuite) TestEvents_RestoreLargeAttachment() {
t := suite.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
@ -245,7 +246,7 @@ func (suite *EventsAPIIntgSuite) TestRestoreLargeAttachment() {
folderName := testdata.DefaultRestoreConfig("eventlargeattachmenttest").Location folderName := testdata.DefaultRestoreConfig("eventlargeattachmenttest").Location
evts := suite.its.ac.Events() evts := suite.its.ac.Events()
calendar, err := evts.CreateContainer(ctx, userID, folderName, "") calendar, err := evts.CreateContainer(ctx, userID, "", folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
tomorrow := time.Now().Add(24 * time.Hour) tomorrow := time.Now().Add(24 * time.Hour)
@ -287,7 +288,7 @@ func (suite *EventsAPIIntgSuite) TestEvents_canFindNonStandardFolder() {
ac := suite.its.ac.Events() ac := suite.its.ac.Events()
rc := testdata.DefaultRestoreConfig("api_calendar_discovery") rc := testdata.DefaultRestoreConfig("api_calendar_discovery")
cal, err := ac.CreateContainer(ctx, suite.its.userID, rc.Location, "") cal, err := ac.CreateContainer(ctx, suite.its.userID, "", rc.Location)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
var ( var (
@ -316,3 +317,91 @@ func (suite *EventsAPIIntgSuite) TestEvents_canFindNonStandardFolder() {
"If this fails, the user's calendars have probably broken, "+ "If this fails, the user's calendars have probably broken, "+
"and the user will need to be rotated") "and the user will need to be rotated")
} }
func (suite *EventsAPIIntgSuite) TestEvents_GetContainerByName() {
table := []struct {
name string
expectErr assert.ErrorAssertionFunc
}{
{
name: "Calendar",
expectErr: assert.NoError,
},
{
name: "smarfs",
expectErr: assert.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
_, err := suite.its.ac.
Events().
GetContainerByName(ctx, suite.its.userID, "", test.name)
test.expectErr(t, err, clues.ToCore(err))
})
}
}
func (suite *EventsAPIIntgSuite) TestEvents_GetContainerByName_mocked() {
c := models.NewCalendar()
c.SetId(ptr.To("id"))
c.SetName(ptr.To("display name"))
table := []struct {
name string
results func(*testing.T) map[string]any
expectErr assert.ErrorAssertionFunc
}{
{
name: "zero",
results: func(t *testing.T) map[string]any {
return parseableToMap(t, models.NewCalendarCollectionResponse())
},
expectErr: assert.Error,
},
{
name: "one",
results: func(t *testing.T) map[string]any {
mfcr := models.NewCalendarCollectionResponse()
mfcr.SetValue([]models.Calendarable{c})
return parseableToMap(t, mfcr)
},
expectErr: assert.NoError,
},
{
name: "two",
results: func(t *testing.T) map[string]any {
mfcr := models.NewCalendarCollectionResponse()
mfcr.SetValue([]models.Calendarable{c, c})
return parseableToMap(t, mfcr)
},
expectErr: assert.NoError,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
defer gock.Off()
interceptV1Path("users", "u", "calendars").
Reply(200).
JSON(test.results(t))
_, err := suite.its.gockAC.
Events().
GetContainerByName(ctx, "u", "", test.name)
test.expectErr(t, err, clues.ToCore(err))
assert.True(t, gock.IsDone())
})
}
}

View File

@ -82,7 +82,7 @@ func (c Mail) DeleteMailFolder(
func (c Mail) CreateContainer( func (c Mail) CreateContainer(
ctx context.Context, ctx context.Context,
userID, containerName, parentContainerID string, userID, parentContainerID, containerName string,
) (graph.Container, error) { ) (graph.Container, error) {
isHidden := false isHidden := false
body := models.NewMailFolder() body := models.NewMailFolder()
@ -165,6 +165,75 @@ func (c Mail) GetContainerByID(
return c.GetFolder(ctx, userID, containerID) return c.GetFolder(ctx, userID, containerID)
} }
// GetContainerByName fetches a folder by name
func (c Mail) GetContainerByName(
ctx context.Context,
userID, parentContainerID, containerName string,
) (graph.Container, error) {
filter := fmt.Sprintf("displayName eq '%s'", containerName)
ctx = clues.Add(ctx, "container_name", containerName)
var (
builder = c.Stable.
Client().
Users().
ByUserId(userID).
MailFolders()
resp models.MailFolderCollectionResponseable
err error
)
if len(parentContainerID) > 0 {
options := &users.ItemMailFoldersItemChildFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemMailFoldersItemChildFoldersRequestBuilderGetQueryParameters{
Filter: &filter,
},
}
resp, err = builder.
ByMailFolderId(parentContainerID).
ChildFolders().
Get(ctx, options)
} else {
options := &users.ItemMailFoldersRequestBuilderGetRequestConfiguration{
QueryParameters: &users.ItemMailFoldersRequestBuilderGetQueryParameters{
Filter: &filter,
},
}
resp, err = builder.Get(ctx, options)
}
if err != nil {
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
gv := resp.GetValue()
if len(gv) == 0 {
return nil, clues.New("container not found").WithClues(ctx)
}
// We only allow the api to match one container with the provided name.
// Return an error if multiple container exist (unlikely) or if no container
// is found.
if len(gv) != 1 {
return nil, clues.New("unexpected number of folders returned").
With("returned_container_count", len(gv)).
WithClues(ctx)
}
// Sanity check ID and name
container := gv[0]
if err := graph.CheckIDAndName(container); err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return container, nil
}
func (c Mail) MoveContainer( func (c Mail) MoveContainer(
ctx context.Context, ctx context.Context,
userID, containerID string, userID, containerID string,

View File

@ -14,12 +14,10 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock" exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
"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/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control/testdata" "github.com/alcionai/corso/src/pkg/control/testdata"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api" "github.com/alcionai/corso/src/pkg/services/m365/api"
"github.com/alcionai/corso/src/pkg/services/m365/api/mock"
) )
type MailAPIUnitSuite struct { type MailAPIUnitSuite struct {
@ -189,9 +187,7 @@ func (suite *MailAPIUnitSuite) TestBytesToMessagable() {
type MailAPIIntgSuite struct { type MailAPIIntgSuite struct {
tester.Suite tester.Suite
credentials account.M365Config its intgTesterSetup
ac api.Client
user string
} }
// We do end up mocking the actual request, but creating the rest // We do end up mocking the actual request, but creating the rest
@ -205,17 +201,7 @@ func TestMailAPIIntgSuite(t *testing.T) {
} }
func (suite *MailAPIIntgSuite) SetupSuite() { func (suite *MailAPIIntgSuite) SetupSuite() {
t := suite.T() suite.its = newIntegrationTesterSetup(suite.T())
a := tester.NewM365Account(t)
m365, err := a.M365Config()
require.NoError(t, err, clues.ToCore(err))
suite.credentials = m365
suite.ac, err = mock.NewClient(m365)
require.NoError(t, err, clues.ToCore(err))
suite.user = tester.M365UserID(t)
} }
func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() { func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
@ -353,7 +339,12 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
defer gock.Off() defer gock.Off()
tt.setupf() tt.setupf()
item, _, err := suite.ac.Mail().GetItem(ctx, "user", mid, false, fault.New(true)) item, _, err := suite.its.gockAC.Mail().GetItem(
ctx,
"user",
mid,
false,
fault.New(true))
tt.expect(t, err) tt.expect(t, err)
it, ok := item.(models.Messageable) it, ok := item.(models.Messageable)
@ -381,7 +372,7 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
} }
} }
func (suite *MailAPIIntgSuite) TestRestoreLargeAttachment() { func (suite *MailAPIIntgSuite) TestMail_RestoreLargeAttachment() {
t := suite.T() t := suite.T()
ctx, flush := tester.NewContext(t) ctx, flush := tester.NewContext(t)
@ -390,7 +381,7 @@ func (suite *MailAPIIntgSuite) TestRestoreLargeAttachment() {
userID := tester.M365UserID(suite.T()) userID := tester.M365UserID(suite.T())
folderName := testdata.DefaultRestoreConfig("maillargeattachmenttest").Location folderName := testdata.DefaultRestoreConfig("maillargeattachmenttest").Location
msgs := suite.ac.Mail() msgs := suite.its.ac.Mail()
mailfolder, err := msgs.CreateMailFolder(ctx, userID, folderName) mailfolder, err := msgs.CreateMailFolder(ctx, userID, folderName)
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
@ -411,3 +402,127 @@ func (suite *MailAPIIntgSuite) TestRestoreLargeAttachment() {
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
require.NotEmpty(t, id, "empty id for large attachment") require.NotEmpty(t, id, "empty id for large attachment")
} }
func (suite *MailAPIIntgSuite) TestMail_GetContainerByName() {
var (
t = suite.T()
acm = suite.its.ac.Mail()
rc = testdata.DefaultRestoreConfig("mail_get_container_by_name")
)
ctx, flush := tester.NewContext(t)
defer flush()
parent, err := acm.CreateContainer(ctx, suite.its.userID, "msgfolderroot", rc.Location)
require.NoError(t, err, clues.ToCore(err))
table := []struct {
name string
parentContainerID string
expectErr assert.ErrorAssertionFunc
}{
{
name: "Inbox",
expectErr: assert.NoError,
},
{
name: "smarfs",
expectErr: assert.Error,
},
{
name: rc.Location,
parentContainerID: ptr.Val(parent.GetId()),
expectErr: assert.Error,
},
{
name: "Inbox",
parentContainerID: ptr.Val(parent.GetId()),
expectErr: assert.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
_, err := acm.GetContainerByName(ctx, suite.its.userID, test.parentContainerID, test.name)
test.expectErr(t, err, clues.ToCore(err))
})
}
suite.Run("child folder with same name", func() {
pid := ptr.Val(parent.GetId())
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
child, err := acm.CreateContainer(ctx, suite.its.userID, pid, rc.Location)
require.NoError(t, err, clues.ToCore(err))
result, err := acm.GetContainerByName(ctx, suite.its.userID, pid, rc.Location)
assert.NoError(t, err, clues.ToCore(err))
assert.Equal(t, ptr.Val(child.GetId()), ptr.Val(result.GetId()))
})
}
func (suite *MailAPIIntgSuite) TestMail_GetContainerByName_mocked() {
mf := models.NewMailFolder()
mf.SetId(ptr.To("id"))
mf.SetDisplayName(ptr.To("display name"))
table := []struct {
name string
results func(*testing.T) map[string]any
expectErr assert.ErrorAssertionFunc
}{
{
name: "zero",
results: func(t *testing.T) map[string]any {
return parseableToMap(t, models.NewMailFolderCollectionResponse())
},
expectErr: assert.Error,
},
{
name: "one",
results: func(t *testing.T) map[string]any {
mfcr := models.NewMailFolderCollectionResponse()
mfcr.SetValue([]models.MailFolderable{mf})
return parseableToMap(t, mfcr)
},
expectErr: assert.NoError,
},
{
name: "two",
results: func(t *testing.T) map[string]any {
mfcr := models.NewMailFolderCollectionResponse()
mfcr.SetValue([]models.MailFolderable{mf, mf})
return parseableToMap(t, mfcr)
},
expectErr: assert.Error,
},
}
for _, test := range table {
suite.Run(test.name, func() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
defer gock.Off()
interceptV1Path("users", "u", "mailFolders").
Reply(200).
JSON(test.results(t))
_, err := suite.its.gockAC.
Mail().
GetContainerByName(ctx, "u", "", test.name)
test.expectErr(t, err, clues.ToCore(err))
assert.True(t, gock.IsDone())
})
}
}