diff --git a/src/cli/backup/sharepoint.go b/src/cli/backup/sharepoint.go index f20f0bcab..51d11881e 100644 --- a/src/cli/backup/sharepoint.go +++ b/src/cli/backup/sharepoint.go @@ -183,7 +183,7 @@ func createSharePointCmd(cmd *cobra.Command, args []string) error { sel := sharePointBackupCreateSelectors(site) - sites, err := m365.Sites(ctx, acct) + sites, err := m365.SiteIDs(ctx, acct) if err != nil { return Only(ctx, errors.Wrap(err, "Failed to retrieve SharePoint sites")) } diff --git a/src/internal/connector/data_collections.go b/src/internal/connector/data_collections.go index 81dcc580b..a17021312 100644 --- a/src/internal/connector/data_collections.go +++ b/src/internal/connector/data_collections.go @@ -29,7 +29,7 @@ func (gc *GraphConnector) DataCollections(ctx context.Context, sels selectors.Se ctx, end := D.Span(ctx, "gc:dataCollections", D.Index("service", sels.Service.String())) defer end() - err := verifyBackupInputs(sels, gc.GetUsers(), gc.GetSiteIds()) + err := verifyBackupInputs(sels, gc.GetUsers(), gc.GetSiteIDs()) if err != nil { return nil, err } @@ -40,7 +40,7 @@ func (gc *GraphConnector) DataCollections(ctx context.Context, sels selectors.Se case selectors.ServiceOneDrive: return gc.OneDriveDataCollections(ctx, sels) case selectors.ServiceSharePoint: - colls, err := sharepoint.DataCollections(ctx, sels, gc.GetSiteIds(), gc.credentials.AzureTenantID, gc) + colls, err := sharepoint.DataCollections(ctx, sels, gc.GetSiteIDs(), gc.credentials.AzureTenantID, gc) if err != nil { return nil, err } diff --git a/src/internal/connector/graph_connector.go b/src/internal/connector/graph_connector.go index e7aec953a..33ce62a8a 100644 --- a/src/internal/connector/graph_connector.go +++ b/src/internal/connector/graph_connector.go @@ -25,6 +25,7 @@ import ( "github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/control" + "github.com/alcionai/corso/src/pkg/filters" "github.com/alcionai/corso/src/pkg/selectors" ) @@ -169,7 +170,7 @@ func (gc *GraphConnector) setTenantUsers(ctx context.Context) error { return nil } -// GetUsers returns the email address of users within tenant. +// GetUsers returns the email address of users within the tenant. func (gc *GraphConnector) GetUsers() []string { return buildFromMap(true, gc.Users) } @@ -218,7 +219,7 @@ func identifySite(item any) (string, string, error) { } if m.GetName() == nil { - // the built-in site at "htps://{tenant-domain}/search" never has a name. + // the built-in site at "https://{tenant-domain}/search" never has a name. if m.GetWebUrl() != nil && strings.HasSuffix(*m.GetWebUrl(), "/search") { return "", "", errKnownSkippableCase } @@ -232,19 +233,55 @@ func identifySite(item any) (string, string, error) { return "", "", errKnownSkippableCase } - return *m.GetName(), *m.GetId(), nil + return *m.GetWebUrl(), *m.GetId(), nil } -// GetSites returns the siteIDs of sharepoint sites within tenant. -func (gc *GraphConnector) GetSites() []string { +// GetSiteWebURLs returns the WebURLs of sharepoint sites within the tenant. +func (gc *GraphConnector) GetSiteWebURLs() []string { return buildFromMap(true, gc.Sites) } -// GetSiteIds returns the M365 id for the user -func (gc *GraphConnector) GetSiteIds() []string { +// GetSiteIds returns the canonical site IDs in the tenant +func (gc *GraphConnector) GetSiteIDs() []string { return buildFromMap(false, gc.Sites) } +// UnionSiteIDsAndWebURLs reduces the id and url slices into a single slice of site IDs. +// WebURLs will run as a path-suffix style matcher. Callers may provide partial urls, though +// each element in the url must fully match. Ex: the webURL value "foo" will match "www.ex.com/foo", +// but not match "www.ex.com/foobar". +// The returned IDs are reduced to a set of unique values. +func (gc *GraphConnector) UnionSiteIDsAndWebURLs(ctx context.Context, ids, urls []string) ([]string, error) { + if len(gc.Sites) == 0 { + if err := gc.setTenantSites(ctx); err != nil { + return nil, err + } + } + + idm := map[string]struct{}{} + + for _, id := range ids { + idm[id] = struct{}{} + } + + match := filters.PathSuffix(urls) + + for url, id := range gc.Sites { + if !match.Compare(url) { + continue + } + + idm[id] = struct{}{} + } + + idsl := make([]string, 0, len(idm)) + for id := range idm { + idsl = append(idsl, id) + } + + return idsl, nil +} + // buildFromMap helper function for returning []string from map. // Returns list of keys iff true; otherwise returns a list of values func buildFromMap(isKey bool, mapping map[string]string) []string { diff --git a/src/internal/connector/graph_connector_test.go b/src/internal/connector/graph_connector_test.go index e2935244f..0fbf0db03 100644 --- a/src/internal/connector/graph_connector_test.go +++ b/src/internal/connector/graph_connector_test.go @@ -20,6 +20,115 @@ import ( "github.com/alcionai/corso/src/pkg/selectors" ) +// --------------------------------------------------------------------------- +// Unit tests +// --------------------------------------------------------------------------- + +type GraphConnectorUnitSuite struct { + suite.Suite +} + +func TestGraphConnectorUnitSuite(t *testing.T) { + suite.Run(t, new(GraphConnectorUnitSuite)) +} + +func (suite *GraphConnectorUnitSuite) TestUnionSiteIDsAndWebURLs() { + const ( + url1 = "www.foo.com/bar" + url2 = "www.fnords.com/smarf" + path1 = "bar" + path2 = "/smarf" + id1 = "site-id-1" + id2 = "site-id-2" + ) + + gc := &GraphConnector{ + // must be populated, else the func will try to make a graph call + // to retrieve site data. + Sites: map[string]string{ + url1: id1, + url2: id2, + }, + } + + table := []struct { + name string + ids []string + urls []string + expect []string + }{ + { + name: "nil", + }, + { + name: "empty", + ids: []string{}, + urls: []string{}, + expect: []string{}, + }, + { + name: "ids only", + ids: []string{id1, id2}, + urls: []string{}, + expect: []string{id1, id2}, + }, + { + name: "urls only", + ids: []string{}, + urls: []string{url1, url2}, + expect: []string{id1, id2}, + }, + { + name: "url suffix only", + ids: []string{}, + urls: []string{path1, path2}, + expect: []string{id1, id2}, + }, + { + name: "url and suffix overlap", + ids: []string{}, + urls: []string{url1, url2, path1, path2}, + expect: []string{id1, id2}, + }, + { + name: "ids and urls, no overlap", + ids: []string{id1}, + urls: []string{url2}, + expect: []string{id1, id2}, + }, + { + name: "ids and urls, overlap", + ids: []string{id1, id2}, + urls: []string{url1, url2}, + expect: []string{id1, id2}, + }, + { + name: "partial non-match on path", + ids: []string{}, + urls: []string{path1[2:], path2[2:]}, + expect: []string{}, + }, + { + name: "partial non-match on url", + ids: []string{}, + urls: []string{url1[5:], url2[5:]}, + expect: []string{}, + }, + } + for _, test := range table { + suite.T().Run(test.name, func(t *testing.T) { + //nolint + result, err := gc.UnionSiteIDsAndWebURLs(context.Background(), test.ids, test.urls) + assert.NoError(t, err) + assert.ElementsMatch(t, test.expect, result) + }) + } +} + +// --------------------------------------------------------------------------- +// Integration tests +// --------------------------------------------------------------------------- + type GraphConnectorIntegrationSuite struct { suite.Suite connector *GraphConnector diff --git a/src/pkg/services/m365/m365.go b/src/pkg/services/m365/m365.go index 714106e15..e2b8d2d3c 100644 --- a/src/pkg/services/m365/m365.go +++ b/src/pkg/services/m365/m365.go @@ -58,15 +58,24 @@ func UserIDs(ctx context.Context, m365Account account.Account) ([]string, error) return ret, nil } -// Sites returns a list of SharePoint sites in the specified M365 tenant -// TODO: Implement paging support -func Sites(ctx context.Context, m365Account account.Account) ([]string, error) { +// SiteURLs returns a list of SharePoint site WebURLs in the specified M365 tenant +func SiteURLs(ctx context.Context, m365Account account.Account) ([]string, error) { gc, err := connector.NewGraphConnector(ctx, m365Account, connector.Sites) if err != nil { return nil, errors.Wrap(err, "could not initialize M365 graph connection") } - return gc.GetSites(), nil + return gc.GetSiteWebURLs(), nil +} + +// SiteURLs returns a list of SharePoint sites IDs in the specified M365 tenant +func SiteIDs(ctx context.Context, m365Account account.Account) ([]string, error) { + gc, err := connector.NewGraphConnector(ctx, m365Account, connector.Sites) + if err != nil { + return nil, errors.Wrap(err, "could not initialize M365 graph connection") + } + + return gc.GetSiteIDs(), nil } // parseUser extracts information from `models.Userable` we care about