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:
parent
f45aecd5db
commit
0e6ef90e41
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/diagnostics"
|
"github.com/alcionai/corso/src/internal/diagnostics"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"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/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/onedrive"
|
||||||
"github.com/alcionai/corso/src/internal/m365/service/sharepoint"
|
"github.com/alcionai/corso/src/internal/m365/service/sharepoint"
|
||||||
"github.com/alcionai/corso/src/internal/operations/inject"
|
"github.com/alcionai/corso/src/internal/operations/inject"
|
||||||
@ -116,6 +117,18 @@ func (ctrl *Controller) ProduceBackupCollections(
|
|||||||
return nil, nil, false, err
|
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:
|
default:
|
||||||
return nil, nil, false, clues.Wrap(clues.New(service.String()), "service not supported").WithClues(ctx)
|
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.
|
// Exchange and OneDrive user existence now checked in checkServiceEnabled.
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
|
case selectors.ServiceGroups:
|
||||||
|
// TODO(meain): check for group existence.
|
||||||
|
return nil
|
||||||
|
|
||||||
case selectors.ServiceSharePoint:
|
case selectors.ServiceSharePoint:
|
||||||
ids = siteIDs
|
ids = siteIDs
|
||||||
}
|
}
|
||||||
@ -197,8 +214,8 @@ func checkServiceEnabled(
|
|||||||
service path.ServiceType,
|
service path.ServiceType,
|
||||||
resource string,
|
resource string,
|
||||||
) (bool, bool, error) {
|
) (bool, bool, error) {
|
||||||
if service == path.SharePointService {
|
if service == path.SharePointService || service == path.GroupsService {
|
||||||
// No "enabled" check required for sharepoint
|
// No "enabled" check required for sharepoint or groups.
|
||||||
return true, true, nil
|
return true, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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())
|
||||||
|
}
|
||||||
|
|||||||
197
src/internal/m365/collection/drive/group_handler.go
Normal file
197
src/internal/m365/collection/drive/group_handler.go
Normal 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
|
||||||
|
}
|
||||||
@ -25,10 +25,9 @@ import (
|
|||||||
func CollectLibraries(
|
func CollectLibraries(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
bpc inject.BackupProducerConfig,
|
bpc inject.BackupProducerConfig,
|
||||||
ad api.Drives,
|
bh drive.BackupHandler,
|
||||||
tenantID string,
|
tenantID string,
|
||||||
ssmb *prefixmatcher.StringSetMatchBuilder,
|
ssmb *prefixmatcher.StringSetMatchBuilder,
|
||||||
scope selectors.SharePointScope,
|
|
||||||
su support.StatusUpdater,
|
su support.StatusUpdater,
|
||||||
errs *fault.Bus,
|
errs *fault.Bus,
|
||||||
) ([]data.BackupCollection, bool, error) {
|
) ([]data.BackupCollection, bool, error) {
|
||||||
@ -37,13 +36,16 @@ func CollectLibraries(
|
|||||||
var (
|
var (
|
||||||
collections = []data.BackupCollection{}
|
collections = []data.BackupCollection{}
|
||||||
colls = drive.NewCollections(
|
colls = drive.NewCollections(
|
||||||
drive.NewLibraryBackupHandler(ad, scope),
|
bh,
|
||||||
tenantID,
|
tenantID,
|
||||||
bpc.ProtectedResource.ID(),
|
bpc.ProtectedResource.ID(),
|
||||||
su,
|
su,
|
||||||
bpc.Options)
|
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)
|
odcs, canUsePreviousBackup, err := colls.Get(ctx, bpc.MetadataCollections, ssmb, errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, graph.Wrap(ctx, err, "getting library")
|
return nil, false, graph.Wrap(ctx, err, "getting library")
|
||||||
|
|||||||
@ -170,6 +170,8 @@ func getResourceClient(rc resource.Category, ac api.Client) (*resourceClient, er
|
|||||||
return &resourceClient{enum: rc, getter: ac.Users()}, nil
|
return &resourceClient{enum: rc, getter: ac.Users()}, nil
|
||||||
case resource.Sites:
|
case resource.Sites:
|
||||||
return &resourceClient{enum: rc, getter: ac.Sites()}, nil
|
return &resourceClient{enum: rc, getter: ac.Sites()}, nil
|
||||||
|
case resource.Groups:
|
||||||
|
return &resourceClient{enum: rc, getter: ac.Groups()}, nil
|
||||||
default:
|
default:
|
||||||
return nil, clues.New("unrecognized owner resource enum").With("resource_enum", rc)
|
return nil, clues.New("unrecognized owner resource enum").With("resource_enum", rc)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,4 +6,5 @@ const (
|
|||||||
UnknownResource Category = ""
|
UnknownResource Category = ""
|
||||||
Users Category = "users"
|
Users Category = "users"
|
||||||
Sites Category = "sites"
|
Sites Category = "sites"
|
||||||
|
Groups Category = "groups"
|
||||||
)
|
)
|
||||||
|
|||||||
@ -5,8 +5,12 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/clues"
|
"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/prefixmatcher"
|
||||||
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"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/graph"
|
||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
"github.com/alcionai/corso/src/internal/observe"
|
"github.com/alcionai/corso/src/internal/observe"
|
||||||
@ -56,7 +60,35 @@ func ProduceBackupCollections(
|
|||||||
var dbcs []data.BackupCollection
|
var dbcs []data.BackupCollection
|
||||||
|
|
||||||
switch scope.Category().PathType() {
|
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...)
|
collections = append(collections, dbcs...)
|
||||||
@ -70,7 +102,7 @@ func ProduceBackupCollections(
|
|||||||
collections,
|
collections,
|
||||||
creds.AzureTenantID,
|
creds.AzureTenantID,
|
||||||
bpc.ProtectedResource.ID(),
|
bpc.ProtectedResource.ID(),
|
||||||
path.UnknownService, // path.GroupsService
|
path.GroupsService,
|
||||||
categories,
|
categories,
|
||||||
su,
|
su,
|
||||||
errs)
|
errs)
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
|
"github.com/alcionai/corso/src/internal/common/prefixmatcher"
|
||||||
"github.com/alcionai/corso/src/internal/data"
|
"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/collection/site"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/internal/m365/support"
|
"github.com/alcionai/corso/src/internal/m365/support"
|
||||||
@ -79,10 +80,9 @@ func ProduceBackupCollections(
|
|||||||
spcs, canUsePreviousBackup, err = site.CollectLibraries(
|
spcs, canUsePreviousBackup, err = site.CollectLibraries(
|
||||||
ctx,
|
ctx,
|
||||||
bpc,
|
bpc,
|
||||||
ac.Drives(),
|
drive.NewLibraryBackupHandler(ac.Drives(), scope),
|
||||||
creds.AzureTenantID,
|
creds.AzureTenantID,
|
||||||
ssmb,
|
ssmb,
|
||||||
scope,
|
|
||||||
su,
|
su,
|
||||||
errs)
|
errs)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -30,6 +30,7 @@ const (
|
|||||||
TestCfgGroupID = "m365groupid"
|
TestCfgGroupID = "m365groupid"
|
||||||
TestCfgUserID = "m365userid"
|
TestCfgUserID = "m365userid"
|
||||||
TestCfgSecondaryUserID = "secondarym365userid"
|
TestCfgSecondaryUserID = "secondarym365userid"
|
||||||
|
TestCfgSecondaryGroupID = "secondarym365groupid"
|
||||||
TestCfgTertiaryUserID = "tertiarym365userid"
|
TestCfgTertiaryUserID = "tertiarym365userid"
|
||||||
TestCfgLoadTestUserID = "loadtestm365userid"
|
TestCfgLoadTestUserID = "loadtestm365userid"
|
||||||
TestCfgLoadTestOrgUsers = "loadtestm365orgusers"
|
TestCfgLoadTestOrgUsers = "loadtestm365orgusers"
|
||||||
|
|||||||
@ -241,6 +241,8 @@ func (pb Builder) ToStreamStorePath(
|
|||||||
metadataService = OneDriveMetadataService
|
metadataService = OneDriveMetadataService
|
||||||
case SharePointService:
|
case SharePointService:
|
||||||
metadataService = SharePointMetadataService
|
metadataService = SharePointMetadataService
|
||||||
|
case GroupsService:
|
||||||
|
metadataService = GroupsMetadataService
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
return &dataLayerResourcePath{
|
||||||
@ -282,6 +284,8 @@ func (pb Builder) ToServiceCategoryMetadataPath(
|
|||||||
metadataService = OneDriveMetadataService
|
metadataService = OneDriveMetadataService
|
||||||
case SharePointService:
|
case SharePointService:
|
||||||
metadataService = SharePointMetadataService
|
metadataService = SharePointMetadataService
|
||||||
|
case GroupsService:
|
||||||
|
metadataService = GroupsMetadataService
|
||||||
}
|
}
|
||||||
|
|
||||||
return &dataLayerResourcePath{
|
return &dataLayerResourcePath{
|
||||||
|
|||||||
@ -78,9 +78,11 @@ var serviceCategories = map[ServiceType]map[CategoryType]struct{}{
|
|||||||
},
|
},
|
||||||
GroupsService: {
|
GroupsService: {
|
||||||
ChannelMessagesCategory: {},
|
ChannelMessagesCategory: {},
|
||||||
|
LibrariesCategory: {},
|
||||||
},
|
},
|
||||||
TeamsService: {
|
TeamsService: {
|
||||||
ChannelMessagesCategory: {},
|
ChannelMessagesCategory: {},
|
||||||
|
LibrariesCategory: {},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -13,10 +13,12 @@ var piiSafePathElems = pii.MapWithPlurals(
|
|||||||
UnknownService.String(),
|
UnknownService.String(),
|
||||||
ExchangeService.String(),
|
ExchangeService.String(),
|
||||||
OneDriveService.String(),
|
OneDriveService.String(),
|
||||||
|
GroupsService.String(),
|
||||||
SharePointService.String(),
|
SharePointService.String(),
|
||||||
ExchangeMetadataService.String(),
|
ExchangeMetadataService.String(),
|
||||||
OneDriveMetadataService.String(),
|
OneDriveMetadataService.String(),
|
||||||
SharePointMetadataService.String(),
|
SharePointMetadataService.String(),
|
||||||
|
GroupsMetadataService.String(),
|
||||||
|
|
||||||
// categories
|
// categories
|
||||||
UnknownCategory.String(),
|
UnknownCategory.String(),
|
||||||
|
|||||||
@ -287,47 +287,54 @@ func (suite *DataLayerResourcePath) TestToServiceCategoryMetadataPath() {
|
|||||||
check: assert.Error,
|
check: assert.Error,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Passes",
|
name: "Exchange Contacts",
|
||||||
service: path.ExchangeService,
|
service: path.ExchangeService,
|
||||||
category: path.ContactsCategory,
|
category: path.ContactsCategory,
|
||||||
expectedService: path.ExchangeMetadataService,
|
expectedService: path.ExchangeMetadataService,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Passes",
|
name: "Exchange Events",
|
||||||
service: path.ExchangeService,
|
service: path.ExchangeService,
|
||||||
category: path.EventsCategory,
|
category: path.EventsCategory,
|
||||||
expectedService: path.ExchangeMetadataService,
|
expectedService: path.ExchangeMetadataService,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Passes",
|
name: "OneDrive Files",
|
||||||
service: path.OneDriveService,
|
service: path.OneDriveService,
|
||||||
category: path.FilesCategory,
|
category: path.FilesCategory,
|
||||||
expectedService: path.OneDriveMetadataService,
|
expectedService: path.OneDriveMetadataService,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Passes",
|
name: "SharePoint Libraries",
|
||||||
service: path.SharePointService,
|
service: path.SharePointService,
|
||||||
category: path.LibrariesCategory,
|
category: path.LibrariesCategory,
|
||||||
expectedService: path.SharePointMetadataService,
|
expectedService: path.SharePointMetadataService,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Passes",
|
name: "SharePoint Lists",
|
||||||
service: path.SharePointService,
|
service: path.SharePointService,
|
||||||
category: path.ListsCategory,
|
category: path.ListsCategory,
|
||||||
expectedService: path.SharePointMetadataService,
|
expectedService: path.SharePointMetadataService,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Passes",
|
name: "SharePoint Pages",
|
||||||
service: path.SharePointService,
|
service: path.SharePointService,
|
||||||
category: path.PagesCategory,
|
category: path.PagesCategory,
|
||||||
expectedService: path.SharePointMetadataService,
|
expectedService: path.SharePointMetadataService,
|
||||||
check: assert.NoError,
|
check: assert.NoError,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "Groups Libraries",
|
||||||
|
service: path.GroupsService,
|
||||||
|
category: path.LibrariesCategory,
|
||||||
|
expectedService: path.GroupsMetadataService,
|
||||||
|
check: assert.NoError,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
|
|||||||
@ -205,8 +205,8 @@ func (s *groups) Scopes() []GroupsScope {
|
|||||||
// -------------------
|
// -------------------
|
||||||
// Scope Factories
|
// Scope Factories
|
||||||
|
|
||||||
// Produces one or more Groups site scopes.
|
// Produces one or more Groups scopes.
|
||||||
// One scope is created per site entry.
|
// 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.Any, that slice is reduced to [selectors.Any]
|
||||||
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
|
// If any slice contains selectors.None, that slice is reduced to [selectors.None]
|
||||||
// If any slice is empty, it defaults 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 = append(
|
||||||
scopes,
|
scopes,
|
||||||
|
makeScope[GroupsScope](GroupsLibraryFolder, Any()),
|
||||||
makeScope[GroupsScope](GroupsChannel, Any()))
|
makeScope[GroupsScope](GroupsChannel, Any()))
|
||||||
|
|
||||||
return scopes
|
return scopes
|
||||||
@ -255,6 +256,56 @@ func (s *sharePoint) ChannelMessages(channels, messages []string, opts ...option
|
|||||||
return scopes
|
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
|
// ItemInfo Factories
|
||||||
|
|
||||||
@ -278,6 +329,8 @@ const (
|
|||||||
GroupsGroup groupsCategory = "GroupsGroup"
|
GroupsGroup groupsCategory = "GroupsGroup"
|
||||||
GroupsChannel groupsCategory = "GroupsChannel"
|
GroupsChannel groupsCategory = "GroupsChannel"
|
||||||
GroupsChannelMessage groupsCategory = "GroupsChannelMessage"
|
GroupsChannelMessage groupsCategory = "GroupsChannelMessage"
|
||||||
|
GroupsLibraryFolder groupsCategory = "GroupsLibraryFolder"
|
||||||
|
GroupsLibraryItem groupsCategory = "GroupsLibraryItem"
|
||||||
|
|
||||||
// details.itemInfo comparables
|
// details.itemInfo comparables
|
||||||
|
|
||||||
@ -292,6 +345,10 @@ var groupsLeafProperties = map[categorizer]leafProperty{
|
|||||||
pathKeys: []categorizer{GroupsChannel, GroupsChannelMessage},
|
pathKeys: []categorizer{GroupsChannel, GroupsChannelMessage},
|
||||||
pathType: path.ChannelMessagesCategory,
|
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
|
GroupsGroup: { // the root category must be represented, even though it isn't a leaf
|
||||||
pathKeys: []categorizer{GroupsGroup},
|
pathKeys: []categorizer{GroupsGroup},
|
||||||
pathType: path.UnknownCategory,
|
pathType: path.UnknownCategory,
|
||||||
@ -311,8 +368,10 @@ func (c groupsCategory) leafCat() categorizer {
|
|||||||
switch c {
|
switch c {
|
||||||
// TODO: if channels ever contain more than one type of item,
|
// TODO: if channels ever contain more than one type of item,
|
||||||
// we'll need to fix this up.
|
// we'll need to fix this up.
|
||||||
case GroupsChannel, GroupsChannelMessage, GroupsInfoSiteLibraryDrive:
|
case GroupsChannel, GroupsChannelMessage:
|
||||||
return GroupsChannelMessage
|
return GroupsChannelMessage
|
||||||
|
case GroupsLibraryFolder, GroupsLibraryItem, GroupsInfoSiteLibraryDrive:
|
||||||
|
return GroupsLibraryItem
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
@ -342,7 +401,7 @@ func (c groupsCategory) isLeaf() bool {
|
|||||||
// pathValues transforms the two paths to maps of identified properties.
|
// pathValues transforms the two paths to maps of identified properties.
|
||||||
//
|
//
|
||||||
// Example:
|
// Example:
|
||||||
// [tenantID, service, siteID, category, folder, itemID]
|
// [tenantID, service, groupID, site, siteID, category, folder, itemID]
|
||||||
// => {spFolder: folder, spItemID: itemID}
|
// => {spFolder: folder, spItemID: itemID}
|
||||||
func (c groupsCategory) pathValues(
|
func (c groupsCategory) pathValues(
|
||||||
repo path.Path,
|
repo path.Path,
|
||||||
@ -357,11 +416,14 @@ func (c groupsCategory) pathValues(
|
|||||||
|
|
||||||
switch c {
|
switch c {
|
||||||
case GroupsChannel, GroupsChannelMessage:
|
case GroupsChannel, GroupsChannelMessage:
|
||||||
|
folderCat, itemCat = GroupsChannel, GroupsChannelMessage
|
||||||
|
rFld = ent.Groups.ParentPath
|
||||||
|
case GroupsLibraryFolder, GroupsLibraryItem:
|
||||||
if ent.Groups == nil {
|
if ent.Groups == nil {
|
||||||
return nil, clues.New("no Groups ItemInfo in details")
|
return nil, clues.New("no Groups ItemInfo in details")
|
||||||
}
|
}
|
||||||
|
|
||||||
folderCat, itemCat = GroupsChannel, GroupsChannelMessage
|
folderCat, itemCat = GroupsLibraryFolder, GroupsLibraryItem
|
||||||
rFld = ent.Groups.ParentPath
|
rFld = ent.Groups.ParentPath
|
||||||
|
|
||||||
default:
|
default:
|
||||||
@ -459,7 +521,7 @@ func (s GroupsScope) set(cat groupsCategory, v []string, opts ...option) GroupsS
|
|||||||
os := []option{}
|
os := []option{}
|
||||||
|
|
||||||
switch cat {
|
switch cat {
|
||||||
case GroupsChannel:
|
case GroupsChannel, GroupsLibraryFolder:
|
||||||
os = append(os, pathComparator())
|
os = append(os, pathComparator())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -472,8 +534,12 @@ func (s GroupsScope) setDefaults() {
|
|||||||
case GroupsGroup:
|
case GroupsGroup:
|
||||||
s[GroupsChannel.String()] = passAny
|
s[GroupsChannel.String()] = passAny
|
||||||
s[GroupsChannelMessage.String()] = passAny
|
s[GroupsChannelMessage.String()] = passAny
|
||||||
|
s[GroupsLibraryFolder.String()] = passAny
|
||||||
|
s[GroupsLibraryItem.String()] = passAny
|
||||||
case GroupsChannel:
|
case GroupsChannel:
|
||||||
s[GroupsChannelMessage.String()] = passAny
|
s[GroupsChannelMessage.String()] = passAny
|
||||||
|
case GroupsLibraryFolder:
|
||||||
|
s[GroupsLibraryItem.String()] = passAny
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -494,6 +560,7 @@ func (s groups) Reduce(
|
|||||||
s.Selector,
|
s.Selector,
|
||||||
map[path.CategoryType]groupsCategory{
|
map[path.CategoryType]groupsCategory{
|
||||||
path.ChannelMessagesCategory: GroupsChannelMessage,
|
path.ChannelMessagesCategory: GroupsChannelMessage,
|
||||||
|
path.LibrariesCategory: GroupsLibraryItem,
|
||||||
},
|
},
|
||||||
errs)
|
errs)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ var serviceToPathType = map[service]path.ServiceType{
|
|||||||
ServiceExchange: path.ExchangeService,
|
ServiceExchange: path.ExchangeService,
|
||||||
ServiceOneDrive: path.OneDriveService,
|
ServiceOneDrive: path.OneDriveService,
|
||||||
ServiceSharePoint: path.SharePointService,
|
ServiceSharePoint: path.SharePointService,
|
||||||
|
ServiceGroups: path.GroupsService,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
|
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"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/str"
|
||||||
"github.com/alcionai/corso/src/internal/common/tform"
|
"github.com/alcionai/corso/src/internal/common/tform"
|
||||||
"github.com/alcionai/corso/src/internal/m365/graph"
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
@ -27,7 +28,7 @@ func (c Client) Groups() Groups {
|
|||||||
return Groups{c}
|
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,
|
// The group acts as the protected resource, and all teams data like events,
|
||||||
// drive and mail messages are owned by that group.
|
// drive and mail messages are owned by that group.
|
||||||
|
|
||||||
@ -115,6 +116,30 @@ func (c Groups) GetByID(
|
|||||||
return resp, graph.Stack(ctx, err).OrNil()
|
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
|
// helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@ -167,3 +192,14 @@ func IsTeam(ctx context.Context, mg models.Groupable) bool {
|
|||||||
|
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|||||||
@ -107,7 +107,7 @@ func (suite *GroupsIntgSuite) TestGetAll() {
|
|||||||
Groups().
|
Groups().
|
||||||
GetAll(ctx, fault.New(true))
|
GetAll(ctx, fault.New(true))
|
||||||
require.NoError(t, err)
|
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() {
|
func (suite *GroupsIntgSuite) TestGroups_GetByID() {
|
||||||
@ -122,34 +122,19 @@ func (suite *GroupsIntgSuite) TestGroups_GetByID() {
|
|||||||
expectErr func(*testing.T, error)
|
expectErr func(*testing.T, error)
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "3 part id",
|
name: "valid id",
|
||||||
id: groupID,
|
id: groupID,
|
||||||
expectErr: func(t *testing.T, err error) {
|
expectErr: func(t *testing.T, err error) {
|
||||||
assert.NoError(t, err, clues.ToCore(err))
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "malformed id",
|
name: "invalid id",
|
||||||
id: uuid.NewString(),
|
id: uuid.NewString(),
|
||||||
expectErr: func(t *testing.T, err error) {
|
expectErr: func(t *testing.T, err error) {
|
||||||
assert.Error(t, err, clues.ToCore(err))
|
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 {
|
for _, test := range table {
|
||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user