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(
ctx context.Context,
userID, containerName, _ string, // parent container not used
userID, _, containerName string, // parent container not used
) (graph.Container, error) {
return h.ac.CreateContainer(ctx, userID, containerName, "")
return h.ac.CreateContainer(ctx, userID, "", containerName)
}
func (h contactRestoreHandler) containerSearcher() containerByNamer {
return nil
func (h contactRestoreHandler) GetContainerByName(
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

View File

@ -48,13 +48,16 @@ func (h eventRestoreHandler) formatRestoreDestination(
func (h eventRestoreHandler) CreateContainer(
ctx context.Context,
userID, containerName, _ string, // parent container not used
userID, _, containerName string, // parent container not used
) (graph.Container, error) {
return h.ac.CreateContainer(ctx, userID, containerName, "")
return h.ac.CreateContainer(ctx, userID, "", containerName)
}
func (h eventRestoreHandler) containerSearcher() containerByNamer {
return h.ac
func (h eventRestoreHandler) GetContainerByName(
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

View File

@ -86,19 +86,14 @@ type itemRestorer interface {
// produces structs that interface with the graph/cache_container
// CachedContainer interface.
type containerAPI interface {
containerByNamer
// POSTs the creation of a new container
CreateContainer(
ctx context.Context,
userID, containerName, parentContainerID string,
userID, parentContainerID, containerName string,
) (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
// folder for that cache tree), or the default root container
// (if the category uses a root folder that exists above the
@ -110,7 +105,7 @@ type containerByNamer interface {
// searches for a container by name.
GetContainerByName(
ctx context.Context,
userID, containerName string,
userID, parentContainerID, containerName string,
) (graph.Container, error)
}

View File

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

View File

@ -44,6 +44,7 @@ func ConsumeRestoreCollections(
el = errs.Local()
)
// FIXME: should be user name
ctx = clues.Add(ctx, "resource_owner", clues.Hide(userID))
for _, dc := range dcs {
@ -289,7 +290,7 @@ func getOrPopulateContainer(
return cached, nil
}
c, err := ca.CreateContainer(ctx, userID, containerName, containerParentID)
c, err := ca.CreateContainer(ctx, userID, containerParentID, containerName)
// 409 handling case:
// 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,
// leaving our local containerResolver with inconsistent state.
if graph.IsErrFolderExists(err) {
cs := ca.containerSearcher()
if cs != nil {
cc, e := cs.GetContainerByName(ctx, userID, containerName)
c = cc
cc, e := ca.GetContainerByName(ctx, userID, containerParentID, containerName)
if e != nil {
err = clues.Stack(err, e)
} else {
c = cc
err = nil
}
}

View File

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

View File

@ -37,8 +37,8 @@ type Contacts struct {
// If successful, returns the created folder object.
func (c Contacts) CreateContainer(
ctx context.Context,
userID, containerName string,
_ string, // parentContainerID needed for iface, doesn't apply to contacts
// parentContainerID needed for iface, doesn't apply to contacts
userID, _, containerName string,
) (graph.Container, error) {
body := models.NewContactFolder()
body.SetDisplayName(ptr.To(containerName))
@ -117,6 +117,56 @@ func (c Contacts) GetContainerByID(
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(
ctx context.Context,
userID, containerID string,

View File

@ -1,4 +1,4 @@
package api
package api_test
import (
"testing"
@ -7,11 +7,15 @@ import (
"github.com/alcionai/clues"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
"github.com/alcionai/corso/src/internal/tester"
"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 {
@ -64,7 +68,7 @@ func (suite *ContactsAPIUnitSuite) TestContactInfo() {
for _, test := range tests {
suite.Run(test.name, func() {
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() {
t := suite.T()
result, err := BytesToContactable(test.byteArray)
result, err := api.BytesToContactable(test.byteArray)
test.checkError(t, err, clues.ToCore(err))
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/pkg/backup/details"
"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
func (c Events) CreateContainer(
ctx context.Context,
userID, containerName string,
_ string, // parentContainerID needed for iface, doesn't apply to contacts
// parentContainerID needed for iface, doesn't apply to events
userID, _, containerName string,
) (graph.Container, error) {
body := models.NewCalendar()
body.SetName(&containerName)
@ -132,7 +133,8 @@ func (c Events) GetContainerByID(
// GetContainerByName fetches a calendar by name
func (c Events) GetContainerByName(
ctx context.Context,
userID, containerName string,
// parentContainerID needed for iface, doesn't apply to events
userID, _, containerName string,
) (graph.Container, error) {
filter := fmt.Sprintf("name eq '%s'", containerName)
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.
Client().
@ -153,24 +155,25 @@ func (c Events) GetContainerByName(
return nil, graph.Stack(ctx, err).WithClues(ctx)
}
// We only allow the api to match one calendar with provided name.
// Return an error if multiple calendars exist (unlikely) or if no calendar
// is found.
if len(resp.GetValue()) != 1 {
err = clues.New("unexpected number of calendars returned").
With("returned_calendar_count", len(resp.GetValue()))
return nil, err
gv := resp.GetValue()
if len(gv) == 0 {
return nil, clues.New("container not found").WithClues(ctx)
}
// 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
cal := resp.GetValue()[0]
cd := CalendarDisplayable{Calendarable: cal}
cal := gv[0]
container := graph.CalendarDisplayable{Calendarable: cal}
if err := graph.CheckIDAndName(cd); err != nil {
return nil, err
if err := graph.CheckIDAndName(container); err != nil {
return nil, clues.Stack(err).WithClues(ctx)
}
return graph.CalendarDisplayable{Calendarable: cal}, nil
return container, nil
}
func (c Events) PatchCalendar(

View File

@ -5,6 +5,7 @@ import (
"time"
"github.com/alcionai/clues"
"github.com/h2non/gock"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -235,7 +236,7 @@ func (suite *EventsAPIIntgSuite) SetupSuite() {
suite.its = newIntegrationTesterSetup(suite.T())
}
func (suite *EventsAPIIntgSuite) TestRestoreLargeAttachment() {
func (suite *EventsAPIIntgSuite) TestEvents_RestoreLargeAttachment() {
t := suite.T()
ctx, flush := tester.NewContext(t)
@ -245,7 +246,7 @@ func (suite *EventsAPIIntgSuite) TestRestoreLargeAttachment() {
folderName := testdata.DefaultRestoreConfig("eventlargeattachmenttest").Location
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))
tomorrow := time.Now().Add(24 * time.Hour)
@ -287,7 +288,7 @@ func (suite *EventsAPIIntgSuite) TestEvents_canFindNonStandardFolder() {
ac := suite.its.ac.Events()
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))
var (
@ -316,3 +317,91 @@ func (suite *EventsAPIIntgSuite) TestEvents_canFindNonStandardFolder() {
"If this fails, the user's calendars have probably broken, "+
"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(
ctx context.Context,
userID, containerName, parentContainerID string,
userID, parentContainerID, containerName string,
) (graph.Container, error) {
isHidden := false
body := models.NewMailFolder()
@ -165,6 +165,75 @@ func (c Mail) GetContainerByID(
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(
ctx context.Context,
userID, containerID string,

View File

@ -14,12 +14,10 @@ import (
"github.com/alcionai/corso/src/internal/common/ptr"
exchMock "github.com/alcionai/corso/src/internal/m365/exchange/mock"
"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/control/testdata"
"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/mock"
)
type MailAPIUnitSuite struct {
@ -189,9 +187,7 @@ func (suite *MailAPIUnitSuite) TestBytesToMessagable() {
type MailAPIIntgSuite struct {
tester.Suite
credentials account.M365Config
ac api.Client
user string
its intgTesterSetup
}
// 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() {
t := 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)
suite.its = newIntegrationTesterSetup(suite.T())
}
func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
@ -353,7 +339,12 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
defer gock.Off()
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)
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()
ctx, flush := tester.NewContext(t)
@ -390,7 +381,7 @@ func (suite *MailAPIIntgSuite) TestRestoreLargeAttachment() {
userID := tester.M365UserID(suite.T())
folderName := testdata.DefaultRestoreConfig("maillargeattachmenttest").Location
msgs := suite.ac.Mail()
msgs := suite.its.ac.Mail()
mailfolder, err := msgs.CreateMailFolder(ctx, userID, folderName)
require.NoError(t, err, clues.ToCore(err))
@ -411,3 +402,127 @@ func (suite *MailAPIIntgSuite) TestRestoreLargeAttachment() {
require.NoError(t, err, clues.ToCore(err))
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())
})
}
}