Consolidate container resolver code (#1365)

## Description

As the exchange container resolvers are pretty close implementation-wise, consolidate the generic code. Also move some of the tests around to better fit what file has the code being tested

## Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Test
- [ ] 💻 CI/Deployment
- [ ] 🐹 Trivial/Minor

## Issue(s)

* closes #1362 

## Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [ ] 💚 E2E
This commit is contained in:
ashmrtn 2022-11-02 16:06:52 -07:00 committed by GitHub
parent bfab312bb3
commit 524c3ba853
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 667 additions and 571 deletions

View File

@ -15,9 +15,9 @@ import (
var _ graph.ContainerResolver = &contactFolderCache{}
type contactFolderCache struct {
cache map[string]graph.CachedContainer
gs graph.Service
userID, rootID string
*containerResolver
gs graph.Service
userID string
}
func (cfc *contactFolderCache) populateContactRoot(
@ -44,18 +44,14 @@ func (cfc *contactFolderCache) populateContactRoot(
"fetching root contact folder: "+support.ConnectorStackErrorTrace(err))
}
idPtr := f.GetId()
if idPtr == nil || len(*idPtr) == 0 {
return errors.New("root folder has no ID")
}
temp := cacheFolder{
Container: f,
p: path.Builder{}.Append(baseContainerPath...),
}
cfc.cache[*idPtr] = &temp
cfc.rootID = *idPtr
if err := cfc.addFolder(temp); err != nil {
return errors.Wrap(err, "adding cache root")
}
return nil
}
@ -84,7 +80,7 @@ func (cfc *contactFolderCache) Populate(
query, err := cfc.
gs.Client().
UsersById(cfc.userID).
ContactFoldersById(cfc.rootID).
ContactFoldersById(baseID).
ChildFolders().
Get(ctx, nil)
if err != nil {
@ -109,7 +105,11 @@ func (cfc *contactFolderCache) Populate(
}
for _, entry := range containers {
err = cfc.AddToCache(ctx, entry)
temp := cacheFolder{
Container: entry,
}
err = cfc.addFolder(temp)
if err != nil {
errs = support.WrapAndAppend(
"cache build in cfc.Populate",
@ -118,6 +118,14 @@ func (cfc *contactFolderCache) Populate(
}
}
if err := cfc.populatePaths(ctx); err != nil {
errs = support.WrapAndAppend(
"contacts resolver",
err,
errs,
)
}
return errs
}
@ -130,90 +138,9 @@ func (cfc *contactFolderCache) init(
return errors.New("m365 folderID required for base folder")
}
if cfc.cache == nil {
cfc.cache = map[string]graph.CachedContainer{}
if cfc.containerResolver == nil {
cfc.containerResolver = newContainerResolver()
}
return cfc.populateContactRoot(ctx, baseNode, baseContainerPath)
}
func (cfc *contactFolderCache) IDToPath(
ctx context.Context,
folderID string,
) (*path.Builder, error) {
c, ok := cfc.cache[folderID]
if !ok {
return nil, errors.Errorf("contact folder %s not cached", folderID)
}
p := c.Path()
if p != nil {
return p, nil
}
parentPath, err := cfc.IDToPath(ctx, *c.GetParentFolderId())
if err != nil {
return nil, errors.Wrap(err, "retrieving parent folder")
}
fullPath := parentPath.Append(*c.GetDisplayName())
c.SetPath(fullPath)
return fullPath, nil
}
// PathInCache utility function to return m365ID of folder if the pathString
// matches the path of a container within the cache. A boolean function
// accompanies the call to indicate whether the lookup was successful.
func (cfc *contactFolderCache) PathInCache(pathString string) (string, bool) {
if len(pathString) == 0 || cfc.cache == nil {
return "", false
}
for _, contain := range cfc.cache {
if contain.Path() == nil {
continue
}
if contain.Path().String() == pathString {
return *contain.GetId(), true
}
}
return "", false
}
// AddToCache places container into internal cache field.
// @returns error iff input does not possess accessible values.
func (cfc *contactFolderCache) AddToCache(ctx context.Context, f graph.Container) error {
if err := checkRequiredValues(f); err != nil {
return err
}
if _, ok := cfc.cache[*f.GetId()]; ok {
return nil
}
cfc.cache[*f.GetId()] = &cacheFolder{
Container: f,
}
// Populate the path for this entry so calls to PathInCache succeed no matter
// when they're made.
_, err := cfc.IDToPath(ctx, *f.GetId())
if err != nil {
return errors.Wrap(err, "adding cache entry")
}
return nil
}
func (cfc *contactFolderCache) Items() []graph.CachedContainer {
res := make([]graph.CachedContainer, 0, len(cfc.cache))
for _, c := range cfc.cache {
res = append(res, c)
}
return res
}

View File

@ -0,0 +1,136 @@
package exchange
import (
"context"
"github.com/hashicorp/go-multierror"
"github.com/pkg/errors"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/pkg/path"
)
func newContainerResolver() *containerResolver {
return &containerResolver{
cache: map[string]graph.CachedContainer{},
}
}
type containerResolver struct {
cache map[string]graph.CachedContainer
}
func (cr *containerResolver) IDToPath(
ctx context.Context,
folderID string,
) (*path.Builder, error) {
c, ok := cr.cache[folderID]
if !ok {
return nil, errors.Errorf("folder %s not cached", folderID)
}
p := c.Path()
if p != nil {
return p, nil
}
parentPath, err := cr.IDToPath(ctx, *c.GetParentFolderId())
if err != nil {
return nil, errors.Wrap(err, "retrieving parent folder")
}
fullPath := parentPath.Append(*c.GetDisplayName())
c.SetPath(fullPath)
return fullPath, nil
}
// PathInCache utility function to return m365ID of folder if the pathString
// matches the path of a container within the cache. A boolean function
// accompanies the call to indicate whether the lookup was successful.
func (cr *containerResolver) PathInCache(pathString string) (string, bool) {
if len(pathString) == 0 || cr == nil {
return "", false
}
for _, contain := range cr.cache {
if contain.Path() == nil {
continue
}
if contain.Path().String() == pathString {
return *contain.GetId(), true
}
}
return "", false
}
// addFolder adds a folder to the cache with the given ID. If the item is
// already in the cache does nothing. The path for the item is not modified.
func (cr *containerResolver) addFolder(cf cacheFolder) error {
// Only require a non-nil non-empty parent if the path isn't already
// populated.
if cf.p != nil {
if err := checkIDAndName(cf.Container); err != nil {
return errors.Wrap(err, "adding item to cache")
}
} else {
if err := checkRequiredValues(cf.Container); err != nil {
return errors.Wrap(err, "adding item to cache")
}
}
if _, ok := cr.cache[*cf.GetId()]; ok {
return nil
}
cr.cache[*cf.GetId()] = &cf
return nil
}
func (cr *containerResolver) Items() []graph.CachedContainer {
res := make([]graph.CachedContainer, 0, len(cr.cache))
for _, c := range cr.cache {
res = append(res, c)
}
return res
}
// AddToCache adds container to map in field 'cache'
// @returns error iff the required values are not accessible.
func (cr *containerResolver) AddToCache(ctx context.Context, f graph.Container) error {
temp := cacheFolder{
Container: f,
}
if err := cr.addFolder(temp); err != nil {
return errors.Wrap(err, "adding cache folder")
}
// Populate the path for this entry so calls to PathInCache succeed no matter
// when they're made.
_, err := cr.IDToPath(ctx, *f.GetId())
if err != nil {
return errors.Wrap(err, "adding cache entry")
}
return nil
}
func (cr *containerResolver) populatePaths(ctx context.Context) error {
var errs *multierror.Error
// Populate all folder paths.
for _, f := range cr.Items() {
_, err := cr.IDToPath(ctx, *f.GetId())
if err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "populating path"))
}
}
return errs.ErrorOrNil()
}

View File

@ -0,0 +1,400 @@
package exchange
import (
stdpath "path"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/path"
)
type mockContainer struct {
id *string
name *string
parentID *string
}
//nolint:revive
func (m mockContainer) GetId() *string {
return m.id
}
func (m mockContainer) GetDisplayName() *string {
return m.name
}
//nolint:revive
func (m mockContainer) GetParentFolderId() *string {
return m.parentID
}
type FolderCacheUnitSuite struct {
suite.Suite
}
func TestFolderCacheUnitSuite(t *testing.T) {
suite.Run(t, new(FolderCacheUnitSuite))
}
type containerCheckTestInfo struct {
name string
c mockContainer
check assert.ErrorAssertionFunc
}
var (
testID = uuid.NewString()
testName = "foo"
testParentID = uuid.NewString()
emptyString = ""
containerCheckTests = []containerCheckTestInfo{
{
name: "NilID",
c: mockContainer{
id: nil,
name: &testName,
parentID: &testParentID,
},
check: assert.Error,
},
{
name: "NilDisplayName",
c: mockContainer{
id: &testID,
name: nil,
parentID: &testParentID,
},
check: assert.Error,
},
{
name: "EmptyID",
c: mockContainer{
id: &emptyString,
name: &testName,
parentID: &testParentID,
},
check: assert.Error,
},
{
name: "EmptyDisplayName",
c: mockContainer{
id: &testID,
name: &emptyString,
parentID: &testParentID,
},
check: assert.Error,
},
{
name: "AllValues",
c: mockContainer{
id: &testID,
name: &testName,
parentID: &testParentID,
},
check: assert.NoError,
},
}
)
func (suite *FolderCacheUnitSuite) TestCheckIDAndName() {
for _, test := range containerCheckTests {
suite.T().Run(test.name, func(t *testing.T) {
test.check(t, checkIDAndName(test.c))
})
}
}
func (suite *FolderCacheUnitSuite) TestCheckRequiredValues() {
table := []containerCheckTestInfo{
{
name: "NilParentFolderID",
c: mockContainer{
id: &testID,
name: &testName,
parentID: nil,
},
check: assert.Error,
},
{
name: "EmptyParentFolderID",
c: mockContainer{
id: &testID,
name: &testName,
parentID: &emptyString,
},
check: assert.Error,
},
}
table = append(table, containerCheckTests...)
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
test.check(t, checkRequiredValues(test.c))
})
}
}
func (suite *FolderCacheUnitSuite) TestAddFolder() {
table := []struct {
name string
cf cacheFolder
check assert.ErrorAssertionFunc
}{
{
name: "NoParentNoPath",
cf: cacheFolder{
Container: &mockContainer{
id: &testID,
name: &testName,
parentID: nil,
},
p: nil,
},
check: assert.Error,
},
{
name: "NoParentPath",
cf: cacheFolder{
Container: &mockContainer{
id: &testID,
name: &testName,
parentID: nil,
},
p: path.Builder{}.Append("foo"),
},
check: assert.NoError,
},
{
name: "NoName",
cf: cacheFolder{
Container: &mockContainer{
id: &testID,
name: nil,
parentID: &testParentID,
},
p: path.Builder{}.Append("foo"),
},
check: assert.Error,
},
{
name: "NoID",
cf: cacheFolder{
Container: &mockContainer{
id: nil,
name: &testName,
parentID: &testParentID,
},
p: path.Builder{}.Append("foo"),
},
check: assert.Error,
},
{
name: "NoPath",
cf: cacheFolder{
Container: &mockContainer{
id: &testID,
name: &testName,
parentID: &testParentID,
},
p: nil,
},
check: assert.NoError,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
fc := newContainerResolver()
test.check(t, fc.addFolder(test.cf))
})
}
}
func newMockCachedContainer(name string) *mockCachedContainer {
return &mockCachedContainer{
id: uuid.NewString(),
parentID: uuid.NewString(),
displayName: name,
}
}
type mockCachedContainer struct {
id string
parentID string
displayName string
p *path.Builder
expectedPath string
}
//nolint:revive
func (m mockCachedContainer) GetId() *string {
return &m.id
}
//nolint:revive
func (m mockCachedContainer) GetParentFolderId() *string {
return &m.parentID
}
func (m mockCachedContainer) GetDisplayName() *string {
return &m.displayName
}
func (m mockCachedContainer) Path() *path.Builder {
return m.p
}
func (m *mockCachedContainer) SetPath(newPath *path.Builder) {
m.p = newPath
}
// TestConfiguredFolderCacheUnitSuite cannot run its tests in parallel.
type ConfiguredFolderCacheUnitSuite struct {
suite.Suite
fc *containerResolver
allContainers []*mockCachedContainer
}
func (suite *ConfiguredFolderCacheUnitSuite) SetupTest() {
suite.allContainers = []*mockCachedContainer{}
for i := 0; i < 4; i++ {
suite.allContainers = append(
suite.allContainers,
newMockCachedContainer(strings.Repeat("sub", i)+"folder"),
)
}
// Base case for the recursive lookup.
suite.allContainers[0].p = path.Builder{}.Append(suite.allContainers[0].displayName)
suite.allContainers[0].expectedPath = suite.allContainers[0].displayName
for i := 1; i < len(suite.allContainers); i++ {
suite.allContainers[i].parentID = suite.allContainers[i-1].id
suite.allContainers[i].expectedPath = stdpath.Join(
suite.allContainers[i-1].expectedPath,
suite.allContainers[i].displayName,
)
}
suite.fc = newContainerResolver()
for _, c := range suite.allContainers {
suite.fc.cache[c.id] = c
}
}
func TestConfiguredFolderCacheUnitSuite(t *testing.T) {
suite.Run(t, new(ConfiguredFolderCacheUnitSuite))
}
func (suite *ConfiguredFolderCacheUnitSuite) TestPopulatePaths() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
require.NoError(t, suite.fc.populatePaths(ctx))
items := suite.fc.Items()
gotPaths := make([]string, 0, len(items))
for _, i := range items {
gotPaths = append(gotPaths, i.Path().String())
}
expectedPaths := make([]string, 0, len(suite.allContainers))
for _, c := range suite.allContainers {
expectedPaths = append(expectedPaths, c.expectedPath)
}
assert.ElementsMatch(t, expectedPaths, gotPaths)
}
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached() {
ctx, flush := tester.NewContext()
defer flush()
for _, c := range suite.allContainers {
suite.T().Run(*c.GetDisplayName(), func(t *testing.T) {
p, err := suite.fc.IDToPath(ctx, c.id)
require.NoError(t, err)
assert.Equal(t, c.expectedPath, p.String())
})
}
}
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
c := suite.allContainers[len(suite.allContainers)-1]
p, err := suite.fc.IDToPath(ctx, c.id)
require.NoError(t, err)
assert.Equal(t, c.expectedPath, p.String())
c.parentID = "foo"
p, err = suite.fc.IDToPath(ctx, c.id)
require.NoError(t, err)
assert.Equal(t, c.expectedPath, p.String())
}
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderErrorsParentNotFound() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
last := suite.allContainers[len(suite.allContainers)-1]
almostLast := suite.allContainers[len(suite.allContainers)-2]
delete(suite.fc.cache, almostLast.id)
_, err := suite.fc.IDToPath(ctx, last.id)
assert.Error(t, err)
}
func (suite *ConfiguredFolderCacheUnitSuite) TestLookupCachedFolderErrorsNotFound() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
_, err := suite.fc.IDToPath(ctx, "foo")
assert.Error(t, err)
}
func (suite *ConfiguredFolderCacheUnitSuite) TestAddToCache() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
last := suite.allContainers[len(suite.allContainers)-1]
m := newMockCachedContainer("testAddFolder")
m.parentID = last.id
m.expectedPath = stdpath.Join(last.expectedPath, m.displayName)
require.NoError(t, suite.fc.AddToCache(ctx, m))
p, err := suite.fc.IDToPath(ctx, m.id)
require.NoError(t, err)
assert.Equal(t, m.expectedPath, p.String())
}

View File

@ -15,9 +15,9 @@ import (
var _ graph.ContainerResolver = &eventCalendarCache{}
type eventCalendarCache struct {
cache map[string]graph.CachedContainer
gs graph.Service
userID, rootID string
*containerResolver
gs graph.Service
userID string
}
// Populate utility function for populating eventCalendarCache.
@ -28,8 +28,8 @@ func (ecc *eventCalendarCache) Populate(
baseID string,
baseContainerPath ...string,
) error {
if ecc.cache == nil {
ecc.cache = map[string]graph.CachedContainer{}
if ecc.containerResolver == nil {
ecc.containerResolver = newContainerResolver()
}
options, err := optionsForCalendars([]string{"name"})
@ -75,10 +75,25 @@ func (ecc *eventCalendarCache) Populate(
return err
}
for _, containerr := range directories {
if err := ecc.AddToCache(ctx, containerr); err != nil {
for _, container := range directories {
if err := checkIDAndName(container); err != nil {
iterateErr = support.WrapAndAppend(
"failure adding "+*containerr.GetDisplayName(),
"adding folder to cache",
err,
iterateErr,
)
continue
}
temp := cacheFolder{
Container: container,
p: path.Builder{}.Append(*container.GetDisplayName()),
}
if err := ecc.addFolder(temp); err != nil {
iterateErr = support.WrapAndAppend(
"failure adding "+*container.GetDisplayName(),
err,
iterateErr)
}
@ -87,69 +102,28 @@ func (ecc *eventCalendarCache) Populate(
return iterateErr
}
func (ecc *eventCalendarCache) IDToPath(
ctx context.Context,
calendarID string,
) (*path.Builder, error) {
c, ok := ecc.cache[calendarID]
if !ok {
return nil, errors.Errorf("calendar %s not cached", calendarID)
}
p := c.Path()
if p == nil {
// Shouldn't happen
p := path.Builder{}.Append(*c.GetDisplayName())
c.SetPath(p)
}
return p, nil
}
// AddToCache places container into internal cache field. For EventCalendars
// this means that the object has to be transformed prior to calling
// this function.
// AddToCache adds container to map in field 'cache'
// @returns error iff the required values are not accessible.
func (ecc *eventCalendarCache) AddToCache(ctx context.Context, f graph.Container) error {
if err := checkIDAndName(f); err != nil {
return err
return errors.Wrap(err, "adding cache folder")
}
if _, ok := ecc.cache[*f.GetId()]; ok {
return nil
}
ecc.cache[*f.GetId()] = &cacheFolder{
temp := cacheFolder{
Container: f,
p: path.Builder{}.Append(*f.GetDisplayName()),
}
if err := ecc.addFolder(temp); err != nil {
return errors.Wrap(err, "adding cache folder")
}
// Populate the path for this entry so calls to PathInCache succeed no matter
// when they're made.
_, err := ecc.IDToPath(ctx, *f.GetId())
if err != nil {
return errors.Wrap(err, "adding cache entry")
}
return nil
}
func (ecc *eventCalendarCache) PathInCache(pathString string) (string, bool) {
if len(pathString) == 0 || ecc.cache == nil {
return "", false
}
for _, containerr := range ecc.cache {
if containerr.Path() == nil {
continue
}
if containerr.Path().String() == pathString {
return *containerr.GetId(), true
}
}
return "", false
}
func (ecc *eventCalendarCache) Items() []graph.CachedContainer {
res := make([]graph.CachedContainer, 0, len(ecc.cache))
for _, c := range ecc.cache {
res = append(res, c)
}
return res
}

View File

@ -50,72 +50,77 @@ func (suite *CacheResolverSuite) TestPopulate() {
ctx, flush := tester.NewContext()
defer flush()
ecc := eventCalendarCache{
userID: tester.M365UserID(suite.T()),
gs: suite.gs,
eventFunc := func(t *testing.T) graph.ContainerResolver {
return &eventCalendarCache{
userID: tester.M365UserID(t),
gs: suite.gs,
}
}
cfc := contactFolderCache{
userID: tester.M365UserID(suite.T()),
gs: suite.gs,
contactFunc := func(t *testing.T) graph.ContainerResolver {
return &contactFolderCache{
userID: tester.M365UserID(t),
gs: suite.gs,
}
}
tests := []struct {
name, folderName, root, basePath string
resolver graph.ContainerResolver
resolverFunc func(t *testing.T) graph.ContainerResolver
canFind assert.BoolAssertionFunc
}{
{
name: "Default Event Cache",
folderName: DefaultCalendar,
root: DefaultCalendar,
basePath: DefaultCalendar,
resolver: &ecc,
canFind: assert.True,
name: "Default Event Cache",
folderName: DefaultCalendar,
root: DefaultCalendar,
basePath: DefaultCalendar,
resolverFunc: eventFunc,
canFind: assert.True,
},
{
name: "Default Event Folder Hidden",
root: DefaultCalendar,
folderName: DefaultContactFolder,
canFind: assert.False,
resolver: &ecc,
name: "Default Event Folder Hidden",
root: DefaultCalendar,
folderName: DefaultContactFolder,
canFind: assert.False,
resolverFunc: eventFunc,
},
{
name: "Name Not in Cache",
folderName: "testFooBarWhoBar",
root: DefaultCalendar,
canFind: assert.False,
resolver: &ecc,
name: "Name Not in Cache",
folderName: "testFooBarWhoBar",
root: DefaultCalendar,
canFind: assert.False,
resolverFunc: eventFunc,
},
{
name: "Default Contact Cache",
folderName: DefaultContactFolder,
root: DefaultContactFolder,
basePath: DefaultContactFolder,
canFind: assert.True,
resolver: &cfc,
name: "Default Contact Cache",
folderName: DefaultContactFolder,
root: DefaultContactFolder,
basePath: DefaultContactFolder,
canFind: assert.True,
resolverFunc: contactFunc,
},
{
name: "Default Contact Hidden",
folderName: DefaultContactFolder,
root: DefaultContactFolder,
canFind: assert.False,
resolver: &cfc,
name: "Default Contact Hidden",
folderName: DefaultContactFolder,
root: DefaultContactFolder,
canFind: assert.False,
resolverFunc: contactFunc,
},
{
name: "Name Not in Cache",
folderName: "testFooBarWhoBar",
root: DefaultContactFolder,
canFind: assert.False,
resolver: &cfc,
name: "Name Not in Cache",
folderName: "testFooBarWhoBar",
root: DefaultContactFolder,
canFind: assert.False,
resolverFunc: contactFunc,
},
}
for _, test := range tests {
suite.T().Run(test.name, func(t *testing.T) {
require.NoError(t, test.resolver.Populate(ctx, test.root, test.basePath))
_, isFound := test.resolver.PathInCache(test.folderName)
resolver := test.resolverFunc(t)
require.NoError(t, resolver.Populate(ctx, test.root, test.basePath))
_, isFound := resolver.PathInCache(test.folderName)
test.canFind(t, isFound)
assert.Greater(t, len(ecc.cache), 0)
})
}
}

View File

@ -18,9 +18,9 @@ var _ graph.ContainerResolver = &mailFolderCache{}
// cache map of cachedContainers where the key = M365ID
// nameLookup map: Key: DisplayName Value: ID
type mailFolderCache struct {
cache map[string]graph.CachedContainer
gs graph.Service
userID, rootID string
*containerResolver
gs graph.Service
userID string
}
// populateMailRoot fetches and populates the "base" directory from user's inbox.
@ -51,19 +51,14 @@ func (mc *mailFolderCache) populateMailRoot(
return errors.Wrap(err, "fetching root folder"+support.ConnectorStackErrorTrace(err))
}
// Root only needs the ID because we hide it's name for Mail.
idPtr := f.GetId()
if idPtr == nil || len(*idPtr) == 0 {
return errors.New("root folder has no ID")
}
temp := cacheFolder{
Container: f,
p: path.Builder{}.Append(baseContainerPath...),
}
mc.cache[*idPtr] = &temp
mc.rootID = *idPtr
if err := mc.addFolder(temp); err != nil {
return errors.Wrap(err, "initializing mail resolver")
}
return nil
}
@ -86,7 +81,7 @@ func (mc *mailFolderCache) Populate(
gs.
Client().
UsersById(mc.userID).
MailFoldersById(mc.rootID).ChildFolders().
MailFoldersById(baseID).ChildFolders().
Delta()
var errs *multierror.Error
@ -100,7 +95,14 @@ func (mc *mailFolderCache) Populate(
}
for _, f := range resp.GetValue() {
if err := mc.AddToCache(ctx, f); err != nil {
temp := cacheFolder{
Container: f,
}
// Use addFolder instead of AddToCache to be conservative about path
// population. The fetch order of the folders could cause failures while
// trying to resolve paths, so put it off until we've gotten all folders.
if err := mc.addFolder(temp); err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "delta fetch"))
continue
}
@ -117,37 +119,16 @@ func (mc *mailFolderCache) Populate(
query = msfolderdelta.NewDeltaRequestBuilder(link, mc.gs.Adapter())
}
if err := mc.populatePaths(ctx); err != nil {
errs = multierror.Append(errs, errors.Wrap(err, "mail resolver"))
}
return errs.ErrorOrNil()
}
func (mc *mailFolderCache) IDToPath(
ctx context.Context,
folderID string,
) (*path.Builder, error) {
c, ok := mc.cache[folderID]
if !ok {
return nil, errors.Errorf("folder %s not cached", folderID)
}
p := c.Path()
if p != nil {
return p, nil
}
parentPath, err := mc.IDToPath(ctx, *c.GetParentFolderId())
if err != nil {
return nil, errors.Wrap(err, "retrieving parent folder")
}
fullPath := parentPath.Append(*c.GetDisplayName())
c.SetPath(fullPath)
return fullPath, nil
}
// init ensures that the structure's fields are initialized.
// Fields Initialized when cache == nil:
// [mc.cache, mc.rootID]
// [mc.cache]
func (mc *mailFolderCache) init(
ctx context.Context,
baseNode string,
@ -157,64 +138,9 @@ func (mc *mailFolderCache) init(
return errors.New("m365 folder ID required for base folder")
}
if mc.cache == nil {
mc.cache = map[string]graph.CachedContainer{}
if mc.containerResolver == nil {
mc.containerResolver = newContainerResolver()
}
return mc.populateMailRoot(ctx, baseNode, baseContainerPath)
}
// AddToCache adds container to map in field 'cache'
// @returns error iff the required values are not accessible.
func (mc *mailFolderCache) AddToCache(ctx context.Context, f graph.Container) error {
if err := checkRequiredValues(f); err != nil {
return errors.Wrap(err, "object not added to cache")
}
if _, ok := mc.cache[*f.GetId()]; ok {
return nil
}
mc.cache[*f.GetId()] = &cacheFolder{
Container: f,
}
// Populate the path for this entry so calls to PathInCache succeed no matter
// when they're made.
_, err := mc.IDToPath(ctx, *f.GetId())
if err != nil {
return errors.Wrap(err, "adding cache entry")
}
return nil
}
// PathInCache utility function to return m365ID of folder if the pathString
// matches the path of a container within the cache.
func (mc *mailFolderCache) PathInCache(pathString string) (string, bool) {
if len(pathString) == 0 || mc.cache == nil {
return "", false
}
for _, folder := range mc.cache {
if folder.Path() == nil {
continue
}
if folder.Path().String() == pathString {
return *folder.GetId(), true
}
}
return "", false
}
func (mc *mailFolderCache) Items() []graph.CachedContainer {
res := make([]graph.CachedContainer, 0, len(mc.cache))
for _, c := range mc.cache {
res = append(res, c)
}
return res
}

View File

@ -2,17 +2,14 @@ package exchange
import (
stdpath "path"
"strings"
"testing"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/path"
)
const (
@ -27,275 +24,6 @@ const (
expectedFolderPath = "toplevel/subFolder/subsubfolder"
)
type mockContainer struct {
id *string
name *string
parentID *string
}
//nolint:revive
func (m mockContainer) GetId() *string {
return m.id
}
func (m mockContainer) GetDisplayName() *string {
return m.name
}
//nolint:revive
func (m mockContainer) GetParentFolderId() *string {
return m.parentID
}
type MailFolderCacheUnitSuite struct {
suite.Suite
}
func TestMailFolderCacheUnitSuite(t *testing.T) {
suite.Run(t, new(MailFolderCacheUnitSuite))
}
func (suite *MailFolderCacheUnitSuite) TestCheckRequiredValues() {
id := uuid.NewString()
name := "foo"
parentID := uuid.NewString()
emptyString := ""
table := []struct {
name string
c mockContainer
check assert.ErrorAssertionFunc
}{
{
name: "NilID",
c: mockContainer{
id: nil,
name: &name,
parentID: &parentID,
},
check: assert.Error,
},
{
name: "NilDisplayName",
c: mockContainer{
id: &id,
name: nil,
parentID: &parentID,
},
check: assert.Error,
},
{
name: "NilParentFolderID",
c: mockContainer{
id: &id,
name: &name,
parentID: nil,
},
check: assert.Error,
},
{
name: "EmptyID",
c: mockContainer{
id: &emptyString,
name: &name,
parentID: &parentID,
},
check: assert.Error,
},
{
name: "EmptyDisplayName",
c: mockContainer{
id: &id,
name: &emptyString,
parentID: &parentID,
},
check: assert.Error,
},
{
name: "EmptyParentFolderID",
c: mockContainer{
id: &id,
name: &name,
parentID: &emptyString,
},
check: assert.Error,
},
{
name: "AllValues",
c: mockContainer{
id: &id,
name: &name,
parentID: &parentID,
},
check: assert.NoError,
},
}
for _, test := range table {
suite.T().Run(test.name, func(t *testing.T) {
test.check(t, graph.CheckRequiredValues(test.c))
})
}
}
func newMockCachedContainer(name string) *mockCachedContainer {
return &mockCachedContainer{
id: uuid.NewString(),
parentID: uuid.NewString(),
displayName: name,
}
}
type mockCachedContainer struct {
id string
parentID string
displayName string
p *path.Builder
expectedPath string
}
//nolint:revive
func (m mockCachedContainer) GetId() *string {
return &m.id
}
//nolint:revive
func (m mockCachedContainer) GetParentFolderId() *string {
return &m.parentID
}
func (m mockCachedContainer) GetDisplayName() *string {
return &m.displayName
}
func (m mockCachedContainer) Path() *path.Builder {
return m.p
}
func (m *mockCachedContainer) SetPath(newPath *path.Builder) {
m.p = newPath
}
// TestConfiguredMailFolderCacheUnitSuite cannot run its tests in parallel.
type ConfiguredMailFolderCacheUnitSuite struct {
suite.Suite
mc mailFolderCache
allContainers []*mockCachedContainer
}
func (suite *ConfiguredMailFolderCacheUnitSuite) SetupTest() {
suite.allContainers = []*mockCachedContainer{}
for i := 0; i < 4; i++ {
suite.allContainers = append(
suite.allContainers,
newMockCachedContainer(strings.Repeat("sub", i)+"folder"),
)
}
// Base case for the recursive lookup.
suite.allContainers[0].p = path.Builder{}.Append(suite.allContainers[0].displayName)
suite.allContainers[0].expectedPath = suite.allContainers[0].displayName
for i := 1; i < len(suite.allContainers); i++ {
suite.allContainers[i].parentID = suite.allContainers[i-1].id
suite.allContainers[i].expectedPath = stdpath.Join(
suite.allContainers[i-1].expectedPath,
suite.allContainers[i].displayName,
)
}
suite.mc = mailFolderCache{cache: map[string]graph.CachedContainer{}}
for _, c := range suite.allContainers {
suite.mc.cache[c.id] = c
}
}
func TestConfiguredMailFolderCacheUnitSuite(t *testing.T) {
suite.Run(t, new(ConfiguredMailFolderCacheUnitSuite))
}
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderNoPathsCached() {
ctx, flush := tester.NewContext()
defer flush()
for _, c := range suite.allContainers {
suite.T().Run(*c.GetDisplayName(), func(t *testing.T) {
p, err := suite.mc.IDToPath(ctx, c.id)
require.NoError(t, err)
assert.Equal(t, c.expectedPath, p.String())
})
}
}
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderCachesPaths() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
c := suite.allContainers[len(suite.allContainers)-1]
p, err := suite.mc.IDToPath(ctx, c.id)
require.NoError(t, err)
assert.Equal(t, c.expectedPath, p.String())
c.parentID = "foo"
p, err = suite.mc.IDToPath(ctx, c.id)
require.NoError(t, err)
assert.Equal(t, c.expectedPath, p.String())
}
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderErrorsParentNotFound() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
last := suite.allContainers[len(suite.allContainers)-1]
almostLast := suite.allContainers[len(suite.allContainers)-2]
delete(suite.mc.cache, almostLast.id)
_, err := suite.mc.IDToPath(ctx, last.id)
assert.Error(t, err)
}
func (suite *ConfiguredMailFolderCacheUnitSuite) TestLookupCachedFolderErrorsNotFound() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
_, err := suite.mc.IDToPath(ctx, "foo")
assert.Error(t, err)
}
func (suite *ConfiguredMailFolderCacheUnitSuite) TestAddToCache() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
last := suite.allContainers[len(suite.allContainers)-1]
m := newMockCachedContainer("testAddFolder")
m.parentID = last.id
m.expectedPath = stdpath.Join(last.expectedPath, m.displayName)
require.NoError(t, suite.mc.AddToCache(ctx, m))
p, err := suite.mc.IDToPath(ctx, m.id)
require.NoError(t, err)
assert.Equal(t, m.expectedPath, p.String())
}
type MailFolderCacheIntegrationSuite struct {
suite.Suite
gs graph.Service