Create backup collections for Group's default SharePoint site (#4030)

This commit has the initial rough set of changes needed to create collections from the group's default SharePoint site.

This still does not have all the functionality that we need, but the idea was that we could get this in and iterate over time.

<!-- PR description-->

---

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

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

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [x] 🌻 Feature
- [ ] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [ ] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* https://github.com/alcionai/corso/issues/3990

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Abin Simon 2023-08-22 13:28:03 +05:30 committed by GitHub
parent f45aecd5db
commit 0e6ef90e41
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 478 additions and 40 deletions

View File

@ -10,6 +10,7 @@ import (
"github.com/alcionai/corso/src/internal/diagnostics"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/service/exchange"
"github.com/alcionai/corso/src/internal/m365/service/groups"
"github.com/alcionai/corso/src/internal/m365/service/onedrive"
"github.com/alcionai/corso/src/internal/m365/service/sharepoint"
"github.com/alcionai/corso/src/internal/operations/inject"
@ -116,6 +117,18 @@ func (ctrl *Controller) ProduceBackupCollections(
return nil, nil, false, err
}
case path.GroupsService:
colls, ssmb, canUsePreviousBackup, err = groups.ProduceBackupCollections(
ctx,
bpc,
ctrl.AC,
ctrl.credentials,
ctrl.UpdateStatus,
errs)
if err != nil {
return nil, nil, false, err
}
default:
return nil, nil, false, clues.Wrap(clues.New(service.String()), "service not supported").WithClues(ctx)
}
@ -176,6 +189,10 @@ func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error {
// Exchange and OneDrive user existence now checked in checkServiceEnabled.
return nil
case selectors.ServiceGroups:
// TODO(meain): check for group existence.
return nil
case selectors.ServiceSharePoint:
ids = siteIDs
}
@ -197,8 +214,8 @@ func checkServiceEnabled(
service path.ServiceType,
resource string,
) (bool, bool, error) {
if service == path.SharePointService {
// No "enabled" check required for sharepoint
if service == path.SharePointService || service == path.GroupsService {
// No "enabled" check required for sharepoint or groups.
return true, true, nil
}

View File

@ -465,3 +465,85 @@ func (suite *SPCollectionIntgSuite) TestCreateSharePointCollection_Lists() {
}
}
}
// ---------------------------------------------------------------------------
// CreateGroupsCollection tests
// ---------------------------------------------------------------------------
type GroupsCollectionIntgSuite struct {
tester.Suite
connector *Controller
user string
}
func TestGroupsCollectionIntgSuite(t *testing.T) {
suite.Run(t, &GroupsCollectionIntgSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tconfig.M365AcctCredEnvs}),
})
}
func (suite *GroupsCollectionIntgSuite) SetupSuite() {
ctx, flush := tester.NewContext(suite.T())
defer flush()
suite.connector = newController(ctx, suite.T(), resource.Sites, path.GroupsService)
suite.user = tconfig.M365UserID(suite.T())
tester.LogTimeOfTest(suite.T())
}
func (suite *GroupsCollectionIntgSuite) TestCreateGroupsCollection_SharePoint() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
var (
groupID = tconfig.M365GroupID(t)
ctrl = newController(ctx, t, resource.Groups, path.GroupsService)
groupIDs = []string{groupID}
)
id, name, err := ctrl.PopulateProtectedResourceIDAndName(ctx, groupID, nil)
require.NoError(t, err, clues.ToCore(err))
sel := selectors.NewGroupsBackup(groupIDs)
// TODO(meain): make use of selectors
sel.Include(sel.LibraryFolders([]string{"test"}, selectors.PrefixMatch()))
sel.SetDiscreteOwnerIDName(id, name)
bpc := inject.BackupProducerConfig{
LastBackupVersion: version.NoBackup,
Options: control.DefaultOptions(),
ProtectedResource: inMock.NewProvider(id, name),
Selector: sel.Selector,
}
collections, excludes, canUsePreviousBackup, err := ctrl.ProduceBackupCollections(
ctx,
bpc,
fault.New(true))
require.NoError(t, err, clues.ToCore(err))
assert.True(t, canUsePreviousBackup, "can use previous backup")
// No excludes yet as this isn't an incremental backup.
assert.True(t, excludes.Empty())
// we don't know an exact count of drives this will produce,
// but it should be more than one.
assert.Greater(t, len(collections), 1)
for _, coll := range collections {
for object := range coll.Items(ctx, fault.New(true)) {
buf := &bytes.Buffer{}
_, err := buf.ReadFrom(object.ToReader())
assert.NoError(t, err, "reading item", clues.ToCore(err))
}
}
status := ctrl.Wait()
assert.NotZero(t, status.Successes)
t.Log(status.String())
}

View File

@ -0,0 +1,197 @@
package drive
import (
"context"
"net/http"
"strings"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
odConsts "github.com/alcionai/corso/src/internal/m365/service/onedrive/consts"
"github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
var _ BackupHandler = &groupBackupHandler{}
type groupBackupHandler struct {
groupID string
ac api.Drives
scope selectors.GroupsScope
}
func NewGroupBackupHandler(groupID string, ac api.Drives, scope selectors.GroupsScope) groupBackupHandler {
return groupBackupHandler{groupID, ac, scope}
}
func (h groupBackupHandler) Get(
ctx context.Context,
url string,
headers map[string]string,
) (*http.Response, error) {
return h.ac.Get(ctx, url, headers)
}
func (h groupBackupHandler) PathPrefix(
tenantID, resourceOwner, driveID string,
) (path.Path, error) {
return path.Build(
tenantID,
resourceOwner,
path.GroupsService,
path.LibrariesCategory, // TODO(meain)
false,
odConsts.DrivesPathDir,
driveID,
odConsts.RootPathDir)
}
func (h groupBackupHandler) CanonicalPath(
folders *path.Builder,
tenantID, resourceOwner string,
) (path.Path, error) {
// TODO(meain): path fixes: sharepoint site ids should be in the path
return folders.ToDataLayerPath(
tenantID,
h.groupID,
path.GroupsService,
path.LibrariesCategory,
false)
}
func (h groupBackupHandler) ServiceCat() (path.ServiceType, path.CategoryType) {
return path.GroupsService, path.LibrariesCategory
}
func (h groupBackupHandler) NewDrivePager(
resourceOwner string,
fields []string,
) api.DrivePager {
return h.ac.NewSiteDrivePager(resourceOwner, fields)
}
func (h groupBackupHandler) NewItemPager(
driveID, link string,
fields []string,
) api.DriveItemDeltaEnumerator {
return h.ac.NewDriveItemDeltaPager(driveID, link, fields)
}
func (h groupBackupHandler) AugmentItemInfo(
dii details.ItemInfo,
item models.DriveItemable,
size int64,
parentPath *path.Builder,
) details.ItemInfo {
return augmentGroupItemInfo(dii, item, size, parentPath)
}
func (h groupBackupHandler) FormatDisplayPath(
driveName string,
pb *path.Builder,
) string {
return "/" + driveName + "/" + pb.String()
}
func (h groupBackupHandler) NewLocationIDer(
driveID string,
elems ...string,
) details.LocationIDer {
return details.NewSharePointLocationIDer(driveID, elems...)
}
func (h groupBackupHandler) GetItemPermission(
ctx context.Context,
driveID, itemID string,
) (models.PermissionCollectionResponseable, error) {
return h.ac.GetItemPermission(ctx, driveID, itemID)
}
func (h groupBackupHandler) GetItem(
ctx context.Context,
driveID, itemID string,
) (models.DriveItemable, error) {
return h.ac.GetItem(ctx, driveID, itemID)
}
func (h groupBackupHandler) IsAllPass() bool {
// TODO(meain)
return true
}
func (h groupBackupHandler) IncludesDir(dir string) bool {
// TODO(meain)
// return h.scope.Matches(selectors.SharePointGroupFolder, dir)
return true
}
// ---------------------------------------------------------------------------
// Common
// ---------------------------------------------------------------------------
func augmentGroupItemInfo(
dii details.ItemInfo,
item models.DriveItemable,
size int64,
parentPath *path.Builder,
) details.ItemInfo {
var driveName, driveID, creatorEmail string
// TODO: we rely on this info for details/restore lookups,
// so if it's nil we have an issue, and will need an alternative
// way to source the data.
if item.GetCreatedBy() != nil && item.GetCreatedBy().GetUser() != nil {
// User is sometimes not available when created via some
// external applications (like backup/restore solutions)
additionalData := item.GetCreatedBy().GetUser().GetAdditionalData()
ed, ok := additionalData["email"]
if !ok {
ed = additionalData["displayName"]
}
if ed != nil {
creatorEmail = *ed.(*string)
}
}
// gsi := item.GetSharepointIds()
// if gsi != nil {
// siteID = ptr.Val(gsi.GetSiteId())
// weburl = ptr.Val(gsi.GetSiteUrl())
// if len(weburl) == 0 {
// weburl = constructWebURL(item.GetAdditionalData())
// }
// }
if item.GetParentReference() != nil {
driveID = ptr.Val(item.GetParentReference().GetDriveId())
driveName = strings.TrimSpace(ptr.Val(item.GetParentReference().GetName()))
}
var pps string
if parentPath != nil {
pps = parentPath.String()
}
dii.Groups = &details.GroupsInfo{
Created: ptr.Val(item.GetCreatedDateTime()),
DriveID: driveID,
DriveName: driveName,
ItemName: ptr.Val(item.GetName()),
ItemType: details.SharePointLibrary,
Modified: ptr.Val(item.GetLastModifiedDateTime()),
Owner: creatorEmail,
ParentPath: pps,
Size: size,
}
dii.Extension = &details.ExtensionData{}
return dii
}

View File

@ -25,10 +25,9 @@ import (
func CollectLibraries(
ctx context.Context,
bpc inject.BackupProducerConfig,
ad api.Drives,
bh drive.BackupHandler,
tenantID string,
ssmb *prefixmatcher.StringSetMatchBuilder,
scope selectors.SharePointScope,
su support.StatusUpdater,
errs *fault.Bus,
) ([]data.BackupCollection, bool, error) {
@ -37,13 +36,16 @@ func CollectLibraries(
var (
collections = []data.BackupCollection{}
colls = drive.NewCollections(
drive.NewLibraryBackupHandler(ad, scope),
bh,
tenantID,
bpc.ProtectedResource.ID(),
su,
bpc.Options)
)
// TODO(meain): backup resource owner should be group id in case
// of group sharepoint site backup. As of now, we always use
// sharepoint site ids.
odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs)
if err != nil {
return nil, false, graph.Wrap(ctx, err, "getting library")

View File

@ -170,6 +170,8 @@ func getResourceClient(rc resource.Category, ac api.Client) (*resourceClient, er
return &resourceClient{enum: rc, getter: ac.Users()}, nil
case resource.Sites:
return &resourceClient{enum: rc, getter: ac.Sites()}, nil
case resource.Groups:
return &resourceClient{enum: rc, getter: ac.Groups()}, nil
default:
return nil, clues.New("unrecognized owner resource enum").With("resource_enum", rc)
}

View File

@ -6,4 +6,5 @@ const (
UnknownResource Category = ""
Users Category = "users"
Sites Category = "sites"
Groups Category = "groups"
)

View File

@ -5,8 +5,12 @@ import (
"github.com/alcionai/clues"
"github.com/alcionai/corso/src/internal/common/idname"
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/drive"
"github.com/alcionai/corso/src/internal/m365/collection/site"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/support"
"github.com/alcionai/corso/src/internal/observe"
@ -56,7 +60,35 @@ func ProduceBackupCollections(
var dbcs []data.BackupCollection
switch scope.Category().PathType() {
case path.LibrariesCategory: // TODO
case path.LibrariesCategory:
// TODO(meain): Private channels get a separate SharePoint
// site. We should also back those up and not just the
// default one.
resp, err := ac.Groups().GetRootSite(ctx, bpc.ProtectedResource.ID())
if err != nil {
return nil, nil, false, err
}
pr := idname.NewProvider(ptr.Val(resp.GetId()), ptr.Val(resp.GetName()))
sbpc := inject.BackupProducerConfig{
LastBackupVersion: bpc.LastBackupVersion,
Options: bpc.Options,
ProtectedResource: pr,
Selector: bpc.Selector,
}
dbcs, canUsePreviousBackup, err = site.CollectLibraries(
ctx,
sbpc,
drive.NewGroupBackupHandler(bpc.ProtectedResource.ID(), ac.Drives(), scope),
creds.AzureTenantID,
ssmb,
su,
errs)
if err != nil {
el.AddRecoverable(ctx, err)
continue
}
}
collections = append(collections, dbcs...)
@ -70,7 +102,7 @@ func ProduceBackupCollections(
collections,
creds.AzureTenantID,
bpc.ProtectedResource.ID(),
path.UnknownService, // path.GroupsService
path.GroupsService,
categories,
su,
errs)

View File

@ -7,6 +7,7 @@ import (
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
"github.com/alcionai/corso/src/internal/data"
"github.com/alcionai/corso/src/internal/m365/collection/drive"
"github.com/alcionai/corso/src/internal/m365/collection/site"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/internal/m365/support"
@ -79,10 +80,9 @@ func ProduceBackupCollections(
spcs, canUsePreviousBackup, err = site.CollectLibraries(
ctx,
bpc,
ac.Drives(),
drive.NewLibraryBackupHandler(ac.Drives(), scope),
creds.AzureTenantID,
ssmb,
scope,
su,
errs)
if err != nil {

View File

@ -30,6 +30,7 @@ const (
TestCfgGroupID = "m365groupid"
TestCfgUserID = "m365userid"
TestCfgSecondaryUserID = "secondarym365userid"
TestCfgSecondaryGroupID = "secondarym365groupid"
TestCfgTertiaryUserID = "tertiarym365userid"
TestCfgLoadTestUserID = "loadtestm365userid"
TestCfgLoadTestOrgUsers = "loadtestm365orgusers"

View File

@ -241,6 +241,8 @@ func (pb Builder) ToStreamStorePath(
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
case GroupsService:
metadataService = GroupsMetadataService
}
return &dataLayerResourcePath{
@ -282,6 +284,8 @@ func (pb Builder) ToServiceCategoryMetadataPath(
metadataService = OneDriveMetadataService
case SharePointService:
metadataService = SharePointMetadataService
case GroupsService:
metadataService = GroupsMetadataService
}
return &dataLayerResourcePath{

View File

@ -78,9 +78,11 @@ var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
},
GroupsService: {
ChannelMessagesCategory: {},
LibrariesCategory: {},
},
TeamsService: {
ChannelMessagesCategory: {},
LibrariesCategory: {},
},
}

View File

@ -13,10 +13,12 @@ var piiSafePathElems = pii.MapWithPlurals(
UnknownService.String(),
ExchangeService.String(),
OneDriveService.String(),
GroupsService.String(),
SharePointService.String(),
ExchangeMetadataService.String(),
OneDriveMetadataService.String(),
SharePointMetadataService.String(),
GroupsMetadataService.String(),
// categories
UnknownCategory.String(),

View File

@ -287,47 +287,54 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
check: assert.Error,
},
{
name: "Passes",
name: "Exchange Contacts",
service: path.ExchangeService,
category: path.ContactsCategory,
expectedService: path.ExchangeMetadataService,
check: assert.NoError,
},
{
name: "Passes",
name: "Exchange Events",
service: path.ExchangeService,
category: path.EventsCategory,
expectedService: path.ExchangeMetadataService,
check: assert.NoError,
},
{
name: "Passes",
name: "OneDrive Files",
service: path.OneDriveService,
category: path.FilesCategory,
expectedService: path.OneDriveMetadataService,
check: assert.NoError,
},
{
name: "Passes",
name: "SharePoint Libraries",
service: path.SharePointService,
category: path.LibrariesCategory,
expectedService: path.SharePointMetadataService,
check: assert.NoError,
},
{
name: "Passes",
name: "SharePoint Lists",
service: path.SharePointService,
category: path.ListsCategory,
expectedService: path.SharePointMetadataService,
check: assert.NoError,
},
{
name: "Passes",
name: "SharePoint Pages",
service: path.SharePointService,
category: path.PagesCategory,
expectedService: path.SharePointMetadataService,
check: assert.NoError,
},
{
name: "Groups Libraries",
service: path.GroupsService,
category: path.LibrariesCategory,
expectedService: path.GroupsMetadataService,
check: assert.NoError,
},
}
for _, test := range table {

View File

@ -205,8 +205,8 @@ func (s *groups) Scopes() []GroupsScope {
// -------------------
// Scope Factories
// Produces one or more Groups site scopes.
// One scope is created per site entry.
// Produces one or more Groups scopes.
// One scope is created per group entry.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
@ -215,6 +215,7 @@ func (s *groups) AllData() []GroupsScope {
scopes = append(
scopes,
makeScope[GroupsScope](GroupsLibraryFolder, Any()),
makeScope[GroupsScope](GroupsChannel, Any()))
return scopes
@ -255,6 +256,56 @@ func (s *sharePoint) ChannelMessages(channels, messages []string, opts ...option
return scopes
}
// Library produces one or more Group library scopes, where the library
// matches upon a given drive by ID or Name. In order to ensure library selection
// this should always be embedded within the Filter() set; include(Library()) will
// select all items in the library without further filtering.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (s *groups) Library(library string) []GroupsScope {
return []GroupsScope{
makeInfoScope[GroupsScope](
GroupsLibraryItem,
GroupsInfoSiteLibraryDrive,
[]string{library},
filters.Equal),
}
}
// LibraryFolders produces one or more SharePoint libraryFolder scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
func (s *groups) LibraryFolders(libraryFolders []string, opts ...option) []GroupsScope {
var (
scopes = []GroupsScope{}
os = append([]option{pathComparator()}, opts...)
)
scopes = append(
scopes,
makeScope[GroupsScope](GroupsLibraryFolder, libraryFolders, os...))
return scopes
}
// LibraryItems produces one or more Groups library item scopes.
// If any slice contains selectors.Any, that slice is reduced to [selectors.Any]
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
// If any slice is empty, it defaults to [selectors.None]
// options are only applied to the library scopes.
func (s *groups) LibraryItems(libraries, items []string, opts ...option) []GroupsScope {
scopes := []GroupsScope{}
scopes = append(
scopes,
makeScope[GroupsScope](GroupsLibraryItem, items, defaultItemOptions(s.Cfg)...).
set(GroupsLibraryFolder, libraries, opts...))
return scopes
}
// -------------------
// ItemInfo Factories
@ -278,6 +329,8 @@ const (
GroupsGroup groupsCategory = "GroupsGroup"
GroupsChannel groupsCategory = "GroupsChannel"
GroupsChannelMessage groupsCategory = "GroupsChannelMessage"
GroupsLibraryFolder groupsCategory = "GroupsLibraryFolder"
GroupsLibraryItem groupsCategory = "GroupsLibraryItem"
// details.itemInfo comparables
@ -292,6 +345,10 @@ var groupsLeafProperties = map[categorizer]leafProperty{
pathKeys: []categorizer{GroupsChannel, GroupsChannelMessage},
pathType: path.ChannelMessagesCategory,
},
GroupsLibraryItem: {
pathKeys: []categorizer{GroupsLibraryFolder, GroupsLibraryItem},
pathType: path.LibrariesCategory,
},
GroupsGroup: { // the root category must be represented, even though it isn't a leaf
pathKeys: []categorizer{GroupsGroup},
pathType: path.UnknownCategory,
@ -311,8 +368,10 @@ func (c groupsCategory) leafCat() categorizer {
switch c {
// TODO: if channels ever contain more than one type of item,
// we'll need to fix this up.
case GroupsChannel, GroupsChannelMessage, GroupsInfoSiteLibraryDrive:
case GroupsChannel, GroupsChannelMessage:
return GroupsChannelMessage
case GroupsLibraryFolder, GroupsLibraryItem, GroupsInfoSiteLibraryDrive:
return GroupsLibraryItem
}
return c
@ -342,7 +401,7 @@ func (c groupsCategory) isLeaf() bool {
// pathValues transforms the two paths to maps of identified properties.
//
// Example:
// [tenantID, service, siteID, category, folder, itemID]
// [tenantID, service, groupID, site, siteID, category, folder, itemID]
// => {spFolder: folder, spItemID: itemID}
func (c groupsCategory) pathValues(
repo path.Path,
@ -357,11 +416,14 @@ func (c groupsCategory) pathValues(
switch c {
case GroupsChannel, GroupsChannelMessage:
folderCat, itemCat = GroupsChannel, GroupsChannelMessage
rFld = ent.Groups.ParentPath
case GroupsLibraryFolder, GroupsLibraryItem:
if ent.Groups == nil {
return nil, clues.New("no Groups ItemInfo in details")
}
folderCat, itemCat = GroupsChannel, GroupsChannelMessage
folderCat, itemCat = GroupsLibraryFolder, GroupsLibraryItem
rFld = ent.Groups.ParentPath
default:
@ -459,7 +521,7 @@ func (s GroupsScope) set(cat groupsCategory, v []string, opts ...option) GroupsS
os := []option{}
switch cat {
case GroupsChannel:
case GroupsChannel, GroupsLibraryFolder:
os = append(os, pathComparator())
}
@ -472,8 +534,12 @@ func (s GroupsScope) setDefaults() {
case GroupsGroup:
s[GroupsChannel.String()] = passAny
s[GroupsChannelMessage.String()] = passAny
s[GroupsLibraryFolder.String()] = passAny
s[GroupsLibraryItem.String()] = passAny
case GroupsChannel:
s[GroupsChannelMessage.String()] = passAny
case GroupsLibraryFolder:
s[GroupsLibraryItem.String()] = passAny
}
}
@ -494,6 +560,7 @@ func (s groups) Reduce(
s.Selector,
map[path.CategoryType]groupsCategory{
path.ChannelMessagesCategory: GroupsChannelMessage,
path.LibrariesCategory: GroupsLibraryItem,
},
errs)
}

View File

@ -32,6 +32,7 @@ var serviceToPathType = map[service]path.ServiceType{
ServiceExchange: path.ExchangeService,
ServiceOneDrive: path.OneDriveService,
ServiceSharePoint: path.SharePointService,
ServiceGroups: path.GroupsService,
}
var (

View File

@ -7,6 +7,7 @@ import (
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/common/tform"
"github.com/alcionai/corso/src/internal/m365/graph"
@ -27,7 +28,7 @@ func (c Client) Groups() Groups {
return Groups{c}
}
// On creation of each Teams team a corrsponding group gets created.
// On creation of each Teams team a corresponding group gets created.
// The group acts as the protected resource, and all teams data like events,
// drive and mail messages are owned by that group.
@ -115,6 +116,30 @@ func (c Groups) GetByID(
return resp, graph.Stack(ctx, err).OrNil()
}
// GetRootSite retrieves the root site for the group.
func (c Groups) GetRootSite(
ctx context.Context,
identifier string,
) (models.Siteable, error) {
service, err := c.Service()
if err != nil {
return nil, err
}
resp, err := service.
Client().
Groups().
ByGroupId(identifier).
Sites().
BySiteId("root").
Get(ctx, nil)
if err != nil {
return nil, clues.Wrap(err, "getting root site for group")
}
return resp, graph.Stack(ctx, err).OrNil()
}
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
@ -167,3 +192,14 @@ func IsTeam(ctx context.Context, mg models.Groupable) bool {
return false
}
// GetIDAndName looks up the group matching the given ID, and returns
// its canonical ID and the name.
func (c Groups) GetIDAndName(ctx context.Context, groupID string) (string, string, error) {
s, err := c.GetByID(ctx, groupID)
if err != nil {
return "", "", err
}
return ptr.Val(s.GetId()), ptr.Val(s.GetDisplayName()), nil
}

View File

@ -107,7 +107,7 @@ func (suite *GroupsIntgSuite) TestGetAll() {
Groups().
GetAll(ctx, fault.New(true))
require.NoError(t, err)
require.NotZero(t, len(groups), "must find at least one group")
require.NotZero(t, len(groups), "must have at least one group")
}
func (suite *GroupsIntgSuite) TestGroups_GetByID() {
@ -122,34 +122,19 @@ func (suite *GroupsIntgSuite) TestGroups_GetByID() {
expectErr func(*testing.T, error)
}{
{
name: "3 part id",
name: "valid id",
id: groupID,
expectErr: func(t *testing.T, err error) {
assert.NoError(t, err, clues.ToCore(err))
},
},
{
name: "malformed id",
name: "invalid id",
id: uuid.NewString(),
expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
},
},
{
name: "random id",
id: uuid.NewString() + "," + uuid.NewString(),
expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
},
},
{
name: "malformed url",
id: "barunihlda",
expectErr: func(t *testing.T, err error) {
assert.Error(t, err, clues.ToCore(err))
},
},
}
for _, test := range table {
suite.Run(test.name, func() {