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:
parent
6fff3f7d1d
commit
8cc3069a6a
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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())
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user