OneDrive tests for Get() (#2342)

## Description

Create a test case that allows checking most of the Get call (getting
item data is not mocked right now). This allows better checking on
things like:
* having multiple drives to backup
* having items split across multiple delta query pages
* errors returned by the API
* aggregation of delta URLs and folder paths in metadata

Eventually these tests could help with checking:
* consumption of metadata for incremental backups
* consumption of delta tokens for incremental backups

Long-term, we may want to merge these with the UpdateCollections tests
as there is overlap between the two.

We may also want to consider a more stable API for checking the
returned items. Right now it partly relies on being able to cast to a
onedrive.Collection struct instead of using `Items()`

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

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No 

## Type of change

- [ ] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [x] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

## Issue(s)

* #2264 

## Test Plan

- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2023-02-01 13:34:16 -08:00 committed by GitHub
parent 93e8b67d15
commit cf1c80271f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 439 additions and 13 deletions

View File

@ -67,6 +67,12 @@ type Collections struct {
// Not the most ideal, but allows us to change the pager function for testing // Not the most ideal, but allows us to change the pager function for testing
// as needed. This will allow us to mock out some scenarios during testing. // as needed. This will allow us to mock out some scenarios during testing.
drivePagerFunc func(
source driveSource,
servicer graph.Servicer,
resourceOwner string,
fields []string,
) (drivePager, error)
itemPagerFunc func( itemPagerFunc func(
servicer graph.Servicer, servicer graph.Servicer,
driveID, link string, driveID, link string,
@ -89,16 +95,17 @@ func NewCollections(
ctrlOpts control.Options, ctrlOpts control.Options,
) *Collections { ) *Collections {
return &Collections{ return &Collections{
itemClient: itemClient, itemClient: itemClient,
tenant: tenant, tenant: tenant,
resourceOwner: resourceOwner, resourceOwner: resourceOwner,
source: source, source: source,
matcher: matcher, matcher: matcher,
CollectionMap: map[string]data.Collection{}, CollectionMap: map[string]data.Collection{},
itemPagerFunc: defaultItemPager, drivePagerFunc: PagerForSource,
service: service, itemPagerFunc: defaultItemPager,
statusUpdater: statusUpdater, service: service,
ctrl: ctrlOpts, statusUpdater: statusUpdater,
ctrl: ctrlOpts,
} }
} }
@ -250,7 +257,7 @@ func (c *Collections) Get(
} }
// Enumerate drives for the specified resourceOwner // Enumerate drives for the specified resourceOwner
pager, err := PagerForSource(c.source, c.service, c.resourceOwner, nil) pager, err := c.drivePagerFunc(c.source, c.service, c.resourceOwner, nil)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }

View File

@ -1,9 +1,11 @@
package onedrive package onedrive
import ( import (
"context"
"strings" "strings"
"testing" "testing"
"github.com/google/uuid"
"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"
@ -11,6 +13,7 @@ import (
"golang.org/x/exp/maps" "golang.org/x/exp/maps"
"github.com/alcionai/corso/src/internal/connector/graph" "github.com/alcionai/corso/src/internal/connector/graph"
gapi "github.com/alcionai/corso/src/internal/connector/graph/api"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
"github.com/alcionai/corso/src/internal/data" "github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
@ -969,7 +972,419 @@ func (suite *OneDriveCollectionsSuite) TestDeserializeMetadata() {
} }
} }
func driveItem(id string, name string, parentPath string, isFile, isFolder, isPackage bool) models.DriveItemable { type mockDeltaPageLinker struct {
link *string
delta *string
}
func (pl *mockDeltaPageLinker) GetOdataNextLink() *string {
return pl.link
}
func (pl *mockDeltaPageLinker) GetOdataDeltaLink() *string {
return pl.delta
}
type deltaPagerResult struct {
items []models.DriveItemable
nextLink *string
deltaLink *string
err error
}
type mockItemPager struct {
// DriveID -> set of return values for queries for that drive.
toReturn []deltaPagerResult
getIdx int
}
func (p *mockItemPager) GetPage(context.Context) (gapi.DeltaPageLinker, error) {
if len(p.toReturn) <= p.getIdx {
return nil, assert.AnError
}
idx := p.getIdx
p.getIdx++
return &mockDeltaPageLinker{
p.toReturn[idx].nextLink,
p.toReturn[idx].deltaLink,
}, p.toReturn[idx].err
}
func (p *mockItemPager) SetNext(string) {}
func (p *mockItemPager) ValuesIn(gapi.DeltaPageLinker) ([]models.DriveItemable, error) {
idx := p.getIdx
if idx > 0 {
// Return values lag by one since we increment in GetPage().
idx--
}
if len(p.toReturn) <= idx {
return nil, assert.AnError
}
return p.toReturn[idx].items, nil
}
func (suite *OneDriveCollectionsSuite) TestGet() {
anyFolder := (&selectors.OneDriveBackup{}).Folders(selectors.Any())[0]
tenant := "a-tenant"
user := "a-user"
metadataPath, err := path.Builder{}.ToServiceCategoryMetadataPath(
tenant,
user,
path.OneDriveService,
path.FilesCategory,
false,
)
require.NoError(suite.T(), err, "making metadata path")
folderPath := expectedPathAsSlice(
suite.T(),
tenant,
user,
testBaseDrivePath+"/folder",
)[0]
empty := ""
next := "next"
delta := "delta1"
delta2 := "delta2"
driveID1 := uuid.NewString()
drive1 := models.NewDrive()
drive1.SetId(&driveID1)
drive1.SetName(&driveID1)
driveID2 := uuid.NewString()
drive2 := models.NewDrive()
drive2.SetId(&driveID2)
drive2.SetName(&driveID2)
driveBasePath2 := "drive/driveID2/root:"
folderPath2 := expectedPathAsSlice(
suite.T(),
tenant,
user,
driveBasePath2+"/folder",
)[0]
table := []struct {
name string
drives []models.Driveable
items map[string][]deltaPagerResult
errCheck assert.ErrorAssertionFunc
// Collection name -> set of item IDs. We can't check item data because
// that's not mocked out. Metadata is checked separately.
expectedCollections map[string][]string
expectedDeltaURLs map[string]string
expectedFolderPaths map[string]map[string]string
expectedDelList map[string]struct{}
}{
{
name: "OneDrive_OneItemPage_DelFileOnly_NoFolders_NoErrors",
drives: []models.Driveable{drive1},
items: map[string][]deltaPagerResult{
driveID1: {
{
items: []models.DriveItemable{
delItem("file", testBaseDrivePath, true, false, false),
},
deltaLink: &delta,
},
},
},
errCheck: assert.NoError,
expectedCollections: map[string][]string{},
expectedDeltaURLs: map[string]string{
driveID1: delta,
},
expectedFolderPaths: map[string]map[string]string{
// We need an empty map here so deserializing metadata knows the delta
// token for this drive is valid.
driveID1: {},
},
expectedDelList: map[string]struct{}{
"file": {},
},
},
{
name: "OneDrive_OneItemPage_NoFolders_NoErrors",
drives: []models.Driveable{drive1},
items: map[string][]deltaPagerResult{
driveID1: {
{
items: []models.DriveItemable{
driveItem("file", "file", testBaseDrivePath, true, false, false),
},
deltaLink: &delta,
},
},
},
errCheck: assert.NoError,
expectedCollections: map[string][]string{
expectedPathAsSlice(
suite.T(),
tenant,
user,
testBaseDrivePath,
)[0]: {"file"},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
},
expectedFolderPaths: map[string]map[string]string{
// We need an empty map here so deserializing metadata knows the delta
// token for this drive is valid.
driveID1: {},
},
expectedDelList: map[string]struct{}{},
},
{
name: "OneDrive_OneItemPage_NoErrors",
drives: []models.Driveable{drive1},
items: map[string][]deltaPagerResult{
driveID1: {
{
items: []models.DriveItemable{
driveItem("folder", "folder", testBaseDrivePath, false, true, false),
driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false),
},
deltaLink: &delta,
},
},
},
errCheck: assert.NoError,
expectedCollections: map[string][]string{
folderPath: {"file"},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
},
expectedFolderPaths: map[string]map[string]string{
driveID1: {
"folder": folderPath,
},
},
expectedDelList: map[string]struct{}{},
},
{
name: "OneDrive_OneItemPage_EmptyDelta_NoErrors",
drives: []models.Driveable{drive1},
items: map[string][]deltaPagerResult{
driveID1: {
{
items: []models.DriveItemable{
driveItem("folder", "folder", testBaseDrivePath, false, true, false),
driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false),
},
deltaLink: &empty,
},
},
},
errCheck: assert.NoError,
expectedCollections: map[string][]string{
folderPath: {"file"},
},
expectedDeltaURLs: map[string]string{},
expectedFolderPaths: map[string]map[string]string{},
expectedDelList: map[string]struct{}{},
},
{
name: "OneDrive_TwoItemPages_NoErrors",
drives: []models.Driveable{drive1},
items: map[string][]deltaPagerResult{
driveID1: {
{
items: []models.DriveItemable{
driveItem("folder", "folder", testBaseDrivePath, false, true, false),
driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false),
},
nextLink: &next,
},
{
items: []models.DriveItemable{
driveItem("folder", "folder", testBaseDrivePath, false, true, false),
driveItem("file2", "file2", testBaseDrivePath+"/folder", true, false, false),
},
deltaLink: &delta,
},
},
},
errCheck: assert.NoError,
expectedCollections: map[string][]string{
folderPath: {"file", "file2"},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
},
expectedFolderPaths: map[string]map[string]string{
driveID1: {
"folder": folderPath,
},
},
expectedDelList: map[string]struct{}{},
},
{
name: "TwoDrives_OneItemPageEach_NoErrors",
drives: []models.Driveable{
drive1,
drive2,
},
items: map[string][]deltaPagerResult{
driveID1: {
{
items: []models.DriveItemable{
driveItem("folder", "folder", testBaseDrivePath, false, true, false),
driveItem("file", "file", testBaseDrivePath+"/folder", true, false, false),
},
deltaLink: &delta,
},
},
driveID2: {
{
items: []models.DriveItemable{
driveItem("folder", "folder", driveBasePath2, false, true, false),
driveItem("file", "file", driveBasePath2+"/folder", true, false, false),
},
deltaLink: &delta2,
},
},
},
errCheck: assert.NoError,
expectedCollections: map[string][]string{
folderPath: {"file"},
folderPath2: {"file"},
},
expectedDeltaURLs: map[string]string{
driveID1: delta,
driveID2: delta2,
},
expectedFolderPaths: map[string]map[string]string{
driveID1: {
"folder": folderPath,
},
driveID2: {
"folder": folderPath2,
},
},
expectedDelList: map[string]struct{}{},
},
{
name: "OneDrive_OneItemPage_Errors",
drives: []models.Driveable{drive1},
items: map[string][]deltaPagerResult{
driveID1: {
{
err: assert.AnError,
},
},
},
errCheck: assert.Error,
expectedCollections: nil,
expectedDeltaURLs: nil,
expectedFolderPaths: nil,
expectedDelList: nil,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
ctx, flush := tester.NewContext()
defer flush()
drivePagerFunc := func(
source driveSource,
servicer graph.Servicer,
resourceOwner string,
fields []string,
) (drivePager, error) {
return &mockDrivePager{
toReturn: []pagerResult{
{
drives: test.drives,
},
},
}, nil
}
itemPagerFunc := func(
servicer graph.Servicer,
driveID, link string,
) itemPager {
return &mockItemPager{
toReturn: test.items[driveID],
}
}
c := NewCollections(
graph.HTTPClient(graph.NoTimeout()),
tenant,
user,
OneDriveSource,
testFolderMatcher{anyFolder},
&MockGraphService{},
func(*support.ConnectorOperationStatus) {},
control.Options{},
)
c.drivePagerFunc = drivePagerFunc
c.itemPagerFunc = itemPagerFunc
// TODO(ashmrtn): Allow passing previous metadata.
cols, _, err := c.Get(ctx, nil)
test.errCheck(t, err)
if err != nil {
return
}
for _, baseCol := range cols {
folderPath := baseCol.FullPath().String()
if folderPath == metadataPath.String() {
deltas, paths, err := deserializeMetadata(ctx, []data.Collection{baseCol})
if !assert.NoError(t, err, "deserializing metadata") {
continue
}
assert.Equal(t, test.expectedDeltaURLs, deltas)
assert.Equal(t, test.expectedFolderPaths, paths)
continue
}
// TODO(ashmrtn): We should really be getting items in the collection
// via the Items() channel, but we don't have a way to mock out the
// actual item fetch yet (mostly wiring issues). The lack of that makes
// this check a bit more bittle since internal details can change.
col, ok := baseCol.(*Collection)
require.True(t, ok, "getting onedrive.Collection handle")
itemIDs := make([]string, 0, len(col.driveItems))
for id := range col.driveItems {
itemIDs = append(itemIDs, id)
}
assert.ElementsMatch(t, test.expectedCollections[folderPath], itemIDs)
}
// TODO(ashmrtn): Uncomment this when we begin return the set of items to
// remove from the upcoming backup.
// assert.Equal(t, test.expectedDelList, delList)
})
}
}
func driveItem(
id string,
name string,
parentPath string,
isFile, isFolder, isPackage bool,
) models.DriveItemable {
item := models.NewDriveItem() item := models.NewDriveItem()
item.SetName(&name) item.SetName(&name)
item.SetId(&id) item.SetId(&id)
@ -992,7 +1407,11 @@ func driveItem(id string, name string, parentPath string, isFile, isFolder, isPa
// delItem creates a DriveItemable that is marked as deleted. path must be set // delItem creates a DriveItemable that is marked as deleted. path must be set
// to the base drive path. // to the base drive path.
func delItem(id string, parentPath string, isFile, isFolder, isPackage bool) models.DriveItemable { func delItem(
id string,
parentPath string,
isFile, isFolder, isPackage bool,
) models.DriveItemable {
item := models.NewDriveItem() item := models.NewDriveItem()
item.SetId(&id) item.SetId(&id)
item.SetDeleted(models.NewDeleted()) item.SetDeleted(models.NewDeleted())