Add GetUserInfo to services/m365 (#2991)

Adds an interface for GetUserInfo, which gets
corso-specific metadata for a single user.  Also
does some refactoring around discovery for
better interface consistency, both as variables
and as access layers.

---

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

- [x]  No

#### Type of change

- [x] 🌻 Feature

#### Test Plan

- [x]  Unit test
- [x] 💚 E2E
This commit is contained in:
Keepers 2023-04-06 18:36:50 -06:00 committed by GitHub
parent b953eb1bd5
commit 3cc29649ed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 270 additions and 61 deletions

View File

@ -2,6 +2,7 @@ package backup
import ( import (
"context" "context"
"fmt"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/pkg/errors" "github.com/pkg/errors"
@ -169,6 +170,8 @@ func createExchangeCmd(cmd *cobra.Command, args []string) error {
return Only(ctx, clues.Wrap(err, "Failed to retrieve M365 users")) return Only(ctx, clues.Wrap(err, "Failed to retrieve M365 users"))
} }
fmt.Printf("\n-----\nINS %+v\n-----\n", ins)
selectorSet := []selectors.Selector{} selectorSet := []selectors.Selector{}
for _, discSel := range sel.SplitByResourceOwner(ins.IDs()) { for _, discSel := range sel.SplitByResourceOwner(ins.IDs()) {

View File

@ -293,7 +293,7 @@ func (suite *PreparedBackupExchangeE2ESuite) SetupSuite() {
defer flush() defer flush()
suite.m365UserID = tester.M365UserID(t) suite.m365UserID = strings.ToLower(tester.M365UserID(t))
// init the repo first // init the repo first
suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{}) suite.repo, err = repository.Initialize(ctx, suite.acct, suite.st, control.Options{})

View File

@ -207,7 +207,7 @@ func (suite *BackupDeleteOneDriveE2ESuite) SetupSuite() {
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
var ( var (
m365UserID = tester.M365UserID(t) m365UserID = strings.ToLower(tester.M365UserID(t))
users = []string{m365UserID} users = []string{m365UserID}
idToName = map[string]string{m365UserID: m365UserID} idToName = map[string]string{m365UserID: m365UserID}
nameToID = map[string]string{m365UserID: m365UserID} nameToID = map[string]string{m365UserID: m365UserID}

View File

@ -159,7 +159,7 @@ func (suite *BackupDeleteSharePointE2ESuite) SetupSuite() {
require.NoError(t, err, clues.ToCore(err)) require.NoError(t, err, clues.ToCore(err))
var ( var (
m365SiteID = tester.M365SiteID(t) m365SiteID = strings.ToLower(tester.M365SiteID(t))
sites = []string{m365SiteID} sites = []string{m365SiteID}
idToName = map[string]string{m365SiteID: m365SiteID} idToName = map[string]string{m365SiteID: m365SiteID}
nameToID = map[string]string{m365SiteID: m365SiteID} nameToID = map[string]string{m365SiteID: m365SiteID}

View File

@ -2,6 +2,7 @@ package restore_test
import ( import (
"context" "context"
"strings"
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
@ -73,7 +74,7 @@ func (suite *RestoreExchangeE2ESuite) SetupSuite() {
} }
suite.vpr, suite.cfgFP = tester.MakeTempTestConfigClone(t, force) suite.vpr, suite.cfgFP = tester.MakeTempTestConfigClone(t, force)
suite.m365UserID = tester.M365UserID(t) suite.m365UserID = strings.ToLower(tester.M365UserID(t))
var ( var (
users = []string{suite.m365UserID} users = []string{suite.m365UserID}

View File

@ -1,6 +1,10 @@
package common package common
import "golang.org/x/exp/maps" import (
"strings"
"golang.org/x/exp/maps"
)
type IDNamer interface { type IDNamer interface {
// the canonical id of the thing, generated and usable // the canonical id of the thing, generated and usable
@ -26,13 +30,13 @@ type IDsNames struct {
// IDOf returns the id associated with the given name. // IDOf returns the id associated with the given name.
func (in IDsNames) IDOf(name string) (string, bool) { func (in IDsNames) IDOf(name string) (string, bool) {
id, ok := in.NameToID[name] id, ok := in.NameToID[strings.ToLower(name)]
return id, ok return id, ok
} }
// NameOf returns the name associated with the given id. // NameOf returns the name associated with the given id.
func (in IDsNames) NameOf(id string) (string, bool) { func (in IDsNames) NameOf(id string) (string, bool) {
name, ok := in.IDToName[id] name, ok := in.IDToName[strings.ToLower(id)]
return name, ok return name, ok
} }

View File

@ -8,8 +8,8 @@ import (
"github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/discovery/api"
"github.com/alcionai/corso/src/internal/connector/exchange" "github.com/alcionai/corso/src/internal/connector/exchange"
"github.com/alcionai/corso/src/internal/connector/graph"
"github.com/alcionai/corso/src/internal/connector/onedrive" "github.com/alcionai/corso/src/internal/connector/onedrive"
"github.com/alcionai/corso/src/internal/connector/sharepoint" "github.com/alcionai/corso/src/internal/connector/sharepoint"
"github.com/alcionai/corso/src/internal/connector/support" "github.com/alcionai/corso/src/internal/connector/support"
@ -19,7 +19,6 @@ import (
"github.com/alcionai/corso/src/pkg/backup/details" "github.com/alcionai/corso/src/pkg/backup/details"
"github.com/alcionai/corso/src/pkg/control" "github.com/alcionai/corso/src/pkg/control"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
"github.com/alcionai/corso/src/pkg/path" "github.com/alcionai/corso/src/pkg/path"
"github.com/alcionai/corso/src/pkg/selectors" "github.com/alcionai/corso/src/pkg/selectors"
) )
@ -171,7 +170,7 @@ func verifyBackupInputs(sels selectors.Selector, siteIDs []string) error {
func checkServiceEnabled( func checkServiceEnabled(
ctx context.Context, ctx context.Context,
au api.Users, gi discovery.GetInfoer,
service path.ServiceType, service path.ServiceType,
resource string, resource string,
) (bool, error) { ) (bool, error) {
@ -180,14 +179,13 @@ func checkServiceEnabled(
return true, nil return true, nil
} }
_, info, err := discovery.User(ctx, au, resource) info, err := gi.GetInfo(ctx, resource)
if err != nil { if err != nil {
return false, err return false, err
} }
if _, ok := info.DiscoveredServices[service]; !ok { if !info.ServiceEnabled(service) {
logger.Ctx(ctx).Error("service not enabled") return false, clues.Wrap(graph.ErrServiceNotEnabled, "checking service access")
return false, nil
} }
return true, nil return true, nil

View File

@ -45,6 +45,18 @@ func newUserInfo() *UserInfo {
} }
} }
// ServiceEnabled returns true if the UserInfo has an entry for the
// service. If no entry exists, the service is assumed to not be enabled.
func (ui *UserInfo) ServiceEnabled(service path.ServiceType) bool {
if ui == nil || len(ui.DiscoveredServices) == 0 {
return false
}
_, ok := ui.DiscoveredServices[service]
return ok
}
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// methods // methods
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -160,7 +172,6 @@ func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) {
// TODO: OneDrive // TODO: OneDrive
_, err = c.stable.Client().UsersById(userID).MailFolders().Get(ctx, nil) _, err = c.stable.Client().UsersById(userID).MailFolders().Get(ctx, nil)
if err != nil { if err != nil {
if !graph.IsErrExchangeMailFolderNotFound(err) { if !graph.IsErrExchangeMailFolderNotFound(err) {
return nil, graph.Wrap(ctx, err, "getting user's mail folder") return nil, graph.Wrap(ctx, err, "getting user's mail folder")

View File

@ -20,13 +20,17 @@ type getter interface {
GetByID(context.Context, string) (models.Userable, error) GetByID(context.Context, string) (models.Userable, error)
} }
type getInfoer interface { type GetInfoer interface {
GetInfo(context.Context, string) (*api.UserInfo, error) GetInfo(context.Context, string) (*api.UserInfo, error)
} }
type getWithInfoer interface { type getWithInfoer interface {
getter getter
getInfoer GetInfoer
}
type getAller interface {
GetAll(ctx context.Context, errs *fault.Bus) ([]models.Userable, error)
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -48,25 +52,28 @@ func apiClient(ctx context.Context, acct account.Account) (api.Client, error) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// api // users
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Users fetches all users in the tenant. // Users fetches all users in the tenant.
func Users( func Users(
ctx context.Context, ctx context.Context,
acct account.Account, ga getAller,
errs *fault.Bus, errs *fault.Bus,
) ([]models.Userable, error) { ) ([]models.Userable, error) {
client, err := apiClient(ctx, acct) users, err := ga.GetAll(ctx, errs)
if err != nil { if err != nil {
return nil, err return nil, clues.Wrap(err, "getting all users")
} }
return client.Users().GetAll(ctx, errs) return users, nil
} }
// User fetches a single user's data. func User(
func User(ctx context.Context, gwi getWithInfoer, userID string) (models.Userable, *api.UserInfo, error) { ctx context.Context,
gwi getWithInfoer,
userID string,
) (models.Userable, *api.UserInfo, error) {
u, err := gwi.GetByID(ctx, userID) u, err := gwi.GetByID(ctx, userID)
if err != nil { if err != nil {
if graph.IsErrUserNotFound(err) { if graph.IsErrUserNotFound(err) {
@ -84,6 +91,25 @@ func User(ctx context.Context, gwi getWithInfoer, userID string) (models.Userabl
return u, ui, nil return u, ui, nil
} }
// UserInfo produces extensible user info: metadata that is relevant
// or identified in Corso, but not in m365.
func UserInfo(
ctx context.Context,
gi GetInfoer,
userID string,
) (*api.UserInfo, error) {
ui, err := gi.GetInfo(ctx, userID)
if err != nil {
return nil, clues.Wrap(err, "getting user info")
}
return ui, nil
}
// ---------------------------------------------------------------------------
// sites
// ---------------------------------------------------------------------------
// Sites fetches all sharepoint sites in the tenant // Sites fetches all sharepoint sites in the tenant
func Sites( func Sites(
ctx context.Context, ctx context.Context,

View File

@ -4,15 +4,18 @@ import (
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/google/uuid"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/discovery/api"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/credentials" "github.com/alcionai/corso/src/pkg/credentials"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
) )
type DiscoveryIntegrationSuite struct { type DiscoveryIntegrationSuite struct {
@ -37,7 +40,13 @@ func (suite *DiscoveryIntegrationSuite) TestUsers() {
errs = fault.New(true) errs = fault.New(true)
) )
users, err := discovery.Users(ctx, acct, errs) creds, err := acct.M365Config()
require.NoError(t, err)
cli, err := api.NewClient(creds)
require.NoError(t, err)
users, err := discovery.Users(ctx, cli.Users(), errs)
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
ferrs := errs.Errors() ferrs := errs.Errors()
@ -47,9 +56,6 @@ func (suite *DiscoveryIntegrationSuite) TestUsers() {
} }
func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() { func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() {
ctx, flush := tester.NewContext()
defer flush()
table := []struct { table := []struct {
name string name string
acct func(t *testing.T) account.Account acct func(t *testing.T) account.Account
@ -72,25 +78,23 @@ func (suite *DiscoveryIntegrationSuite) TestUsers_InvalidCredentials() {
return a return a
}, },
}, },
{
name: "Empty Credentials",
acct: func(t *testing.T) account.Account {
// intentionally swallowing the error here
a, _ := account.NewAccount(account.ProviderM365)
return a
},
},
} }
for _, test := range table { for _, test := range table {
suite.Run(test.name, func() { suite.Run(test.name, func() {
var ( ctx, flush := tester.NewContext()
t = suite.T() defer flush()
a = test.acct(t)
errs = fault.New(true)
)
users, err := discovery.Users(ctx, a, errs) t := suite.T()
acct := test.acct(t)
creds, err := acct.M365Config()
require.NoError(t, err)
cli, err := api.NewClient(creds)
require.NoError(t, err)
users, err := discovery.Users(ctx, cli.Users(), fault.New(true))
assert.Empty(t, users, "returned some users") assert.Empty(t, users, "returned some users")
assert.NotNil(t, err) assert.NotNil(t, err)
}) })
@ -166,3 +170,55 @@ func (suite *DiscoveryIntegrationSuite) TestSites_InvalidCredentials() {
}) })
} }
} }
func (suite *DiscoveryIntegrationSuite) TestUserInfo() {
t := suite.T()
acct := tester.NewM365Account(t)
userID := tester.M365UserID(t)
creds, err := acct.M365Config()
require.NoError(t, err)
cli, err := api.NewClient(creds)
require.NoError(t, err)
uapi := cli.Users()
table := []struct {
name string
user string
expect *api.UserInfo
}{
{
name: "standard test user",
user: userID,
expect: &api.UserInfo{
DiscoveredServices: map[path.ServiceType]struct{}{
path.ExchangeService: {},
path.OneDriveService: {},
},
},
},
{
name: "user does not exist",
user: uuid.NewString(),
expect: &api.UserInfo{
DiscoveredServices: map[path.ServiceType]struct{}{
path.OneDriveService: {}, // currently statically populated
},
},
},
}
for _, test := range table {
suite.Run(test.name, func() {
ctx, flush := tester.NewContext()
defer flush()
t := suite.T()
result, err := discovery.UserInfo(ctx, uapi, test.user)
require.NoError(t, err, clues.ToCore(err))
assert.Equal(t, test.expect, result)
})
}
}

View File

@ -61,6 +61,10 @@ var (
// https://learn.microsoft.com/en-us/graph/errors#code-property // https://learn.microsoft.com/en-us/graph/errors#code-property
ErrInvalidDelta = clues.New("invalid delta token") ErrInvalidDelta = clues.New("invalid delta token")
// ErrServiceNotEnabled identifies that a resource owner does not have
// access to a given service.
ErrServiceNotEnabled = clues.New("service is not enabled for that resource owner")
// Timeout errors are identified for tracking the need to retry calls. // Timeout errors are identified for tracking the need to retry calls.
// Other delay errors, like throttling, are already handled by the // Other delay errors, like throttling, are already handled by the
// graph client's built-in retries. // graph client's built-in retries.

View File

@ -152,7 +152,7 @@ func getOwnerIDAndNameFrom(
// and populate with maps as a result. Only // and populate with maps as a result. Only
// return owner, owner as a very last resort. // return owner, owner as a very last resort.
return owner, owner, nil return "", "", clues.New("not found within tenant")
} }
// createService constructor for graphService component // createService constructor for graphService component

View File

@ -56,12 +56,14 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
ins common.IDsNames ins common.IDsNames
expectID string expectID string
expectName string expectName string
expectErr require.ErrorAssertionFunc
}{ }{
{ {
name: "nil ins", name: "nil ins",
owner: ownerID, owner: ownerID,
expectID: ownerID, expectID: "",
expectName: ownerID, expectName: "",
expectErr: require.Error,
}, },
{ {
name: "only id map with owner id", name: "only id map with owner id",
@ -72,6 +74,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
}, },
expectID: ownerID, expectID: ownerID,
expectName: ownerName, expectName: ownerName,
expectErr: require.NoError,
}, },
{ {
name: "only name map with owner id", name: "only name map with owner id",
@ -80,8 +83,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
IDToName: nil, IDToName: nil,
NameToID: nti, NameToID: nti,
}, },
expectID: ownerID, expectID: "",
expectName: ownerID, expectName: "",
expectErr: require.Error,
}, },
{ {
name: "only id map with owner name", name: "only id map with owner name",
@ -90,8 +94,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
IDToName: itn, IDToName: itn,
NameToID: nil, NameToID: nil,
}, },
expectID: ownerName, expectID: "",
expectName: ownerName, expectName: "",
expectErr: require.Error,
}, },
{ {
name: "only name map with owner name", name: "only name map with owner name",
@ -102,6 +107,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
}, },
expectID: ownerID, expectID: ownerID,
expectName: ownerName, expectName: ownerName,
expectErr: require.NoError,
}, },
{ {
name: "both maps with owner id", name: "both maps with owner id",
@ -112,6 +118,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
}, },
expectID: ownerID, expectID: ownerID,
expectName: ownerName, expectName: ownerName,
expectErr: require.NoError,
}, },
{ {
name: "both maps with owner name", name: "both maps with owner name",
@ -122,6 +129,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
}, },
expectID: ownerID, expectID: ownerID,
expectName: ownerName, expectName: ownerName,
expectErr: require.NoError,
}, },
{ {
name: "non-matching maps with owner id", name: "non-matching maps with owner id",
@ -130,8 +138,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
IDToName: map[string]string{"foo": "bar"}, IDToName: map[string]string{"foo": "bar"},
NameToID: map[string]string{"fnords": "smarf"}, NameToID: map[string]string{"fnords": "smarf"},
}, },
expectID: ownerID, expectID: "",
expectName: ownerID, expectName: "",
expectErr: require.Error,
}, },
{ {
name: "non-matching with owner name", name: "non-matching with owner name",
@ -140,8 +149,9 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
IDToName: map[string]string{"foo": "bar"}, IDToName: map[string]string{"foo": "bar"},
NameToID: map[string]string{"fnords": "smarf"}, NameToID: map[string]string{"fnords": "smarf"},
}, },
expectID: ownerName, expectID: "",
expectName: ownerName, expectName: "",
expectErr: require.Error,
}, },
} }
for _, test := range table { for _, test := range table {
@ -152,7 +162,7 @@ func (suite *GraphConnectorUnitSuite) TestPopulateOwnerIDAndNamesFrom() {
) )
id, name, err := gc.PopulateOwnerIDAndNamesFrom(test.owner, test.ins) id, name, err := gc.PopulateOwnerIDAndNamesFrom(test.owner, test.ins)
require.NoError(t, err, clues.ToCore(err)) test.expectErr(t, err, clues.ToCore(err))
assert.Equal(t, test.expectID, id) assert.Equal(t, test.expectID, id)
assert.Equal(t, test.expectName, name) assert.Equal(t, test.expectName, name)
}) })

View File

@ -10,16 +10,34 @@ import (
"github.com/alcionai/corso/src/internal/common" "github.com/alcionai/corso/src/internal/common"
"github.com/alcionai/corso/src/internal/common/ptr" "github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/connector/discovery" "github.com/alcionai/corso/src/internal/connector/discovery"
"github.com/alcionai/corso/src/internal/connector/discovery/api"
"github.com/alcionai/corso/src/pkg/account" "github.com/alcionai/corso/src/pkg/account"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/path"
) )
// ServiceAccess is true if a resource owner is capable of
// accessing or utilizing the specified service.
type ServiceAccess struct {
Exchange bool
// TODO: onedrive, sharepoint
}
// ---------------------------------------------------------------------------
// Users
// ---------------------------------------------------------------------------
// User is the minimal information required to identify and display a user.
type User struct { type User struct {
PrincipalName string PrincipalName string
ID string ID string
Name string Name string
} }
type UserInfo struct {
ServicesEnabled ServiceAccess
}
// UsersCompat returns a list of users in the specified M365 tenant. // UsersCompat returns a list of users in the specified M365 tenant.
// TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault // TODO(ashmrtn): Remove when upstream consumers of the SDK support the fault
// package. // package.
@ -35,9 +53,13 @@ func UsersCompat(ctx context.Context, acct account.Account) ([]*User, error) {
} }
// Users returns a list of users in the specified M365 tenant // Users returns a list of users in the specified M365 tenant
// TODO: Implement paging support
func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) { func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User, error) {
users, err := discovery.Users(ctx, acct, errs) uapi, err := makeUserAPI(acct)
if err != nil {
return nil, clues.Wrap(err, "getting users").WithClues(ctx)
}
users, err := discovery.Users(ctx, uapi, errs)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -47,7 +69,7 @@ func Users(ctx context.Context, acct account.Account, errs *fault.Bus) ([]*User,
for _, u := range users { for _, u := range users {
pu, err := parseUser(u) pu, err := parseUser(u)
if err != nil { if err != nil {
return nil, clues.Wrap(err, "parsing userable") return nil, clues.Wrap(err, "formatting user data")
} }
ret = append(ret, pu) ret = append(ret, pu)
@ -103,8 +125,38 @@ func UsersMap(
return ins, nil return ins, nil
} }
// UserInfo returns the corso-specific set of user metadata.
func GetUserInfo(
ctx context.Context,
acct account.Account,
userID string,
) (*UserInfo, error) {
uapi, err := makeUserAPI(acct)
if err != nil {
return nil, clues.Wrap(err, "getting user info").WithClues(ctx)
}
ui, err := discovery.UserInfo(ctx, uapi, userID)
if err != nil {
return nil, err
}
info := UserInfo{
ServicesEnabled: ServiceAccess{
Exchange: ui.ServiceEnabled(path.ExchangeService),
},
}
return &info, nil
}
// ---------------------------------------------------------------------------
// Sites
// ---------------------------------------------------------------------------
// Site is the minimal information required to identify and display a SharePoint site.
type Site struct { type Site struct {
// WebURL that displays the item in the browser // WebURL is the url for the site, works as an alias for the user name.
WebURL string WebURL string
// ID is of the format: <site collection hostname>.<site collection unique id>.<site unique id> // ID is of the format: <site collection hostname>.<site collection unique id>.<site unique id>
@ -173,3 +225,21 @@ func SitesMap(
return ins, nil return ins, nil
} }
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
func makeUserAPI(acct account.Account) (api.Users, error) {
creds, err := acct.M365Config()
if err != nil {
return api.Users{}, clues.Wrap(err, "getting m365 account creds")
}
cli, err := api.NewClient(creds)
if err != nil {
return api.Users{}, clues.Wrap(err, "constructing api client")
}
return cli.Users(), nil
}

View File

@ -1,14 +1,16 @@
package m365 package m365_test
import ( import (
"testing" "testing"
"github.com/alcionai/clues" "github.com/alcionai/clues"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite" "github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/tester" "github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/pkg/fault" "github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365"
) )
type M365IntegrationSuite struct { type M365IntegrationSuite struct {
@ -33,7 +35,7 @@ func (suite *M365IntegrationSuite) TestUsers() {
acct = tester.NewM365Account(suite.T()) acct = tester.NewM365Account(suite.T())
) )
users, err := Users(ctx, acct, fault.New(true)) users, err := m365.Users(ctx, acct, fault.New(true))
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
assert.NotEmpty(t, users) assert.NotEmpty(t, users)
@ -48,6 +50,30 @@ func (suite *M365IntegrationSuite) TestUsers() {
} }
} }
func (suite *M365IntegrationSuite) TestGetUserInfo() {
ctx, flush := tester.NewContext()
defer flush()
var (
t = suite.T()
acct = tester.NewM365Account(t)
uid = tester.M365UserID(t)
)
info, err := m365.GetUserInfo(ctx, acct, uid)
require.NoError(t, err, clues.ToCore(err))
require.NotNil(t, info)
require.NotEmpty(t, info)
expect := &m365.UserInfo{
ServicesEnabled: m365.ServiceAccess{
Exchange: true,
},
}
assert.Equal(t, expect, info)
}
func (suite *M365IntegrationSuite) TestSites() { func (suite *M365IntegrationSuite) TestSites() {
ctx, flush := tester.NewContext() ctx, flush := tester.NewContext()
defer flush() defer flush()
@ -57,7 +83,7 @@ func (suite *M365IntegrationSuite) TestSites() {
acct = tester.NewM365Account(suite.T()) acct = tester.NewM365Account(suite.T())
) )
sites, err := Sites(ctx, acct, fault.New(true)) sites, err := m365.Sites(ctx, acct, fault.New(true))
assert.NoError(t, err, clues.ToCore(err)) assert.NoError(t, err, clues.ToCore(err))
assert.NotEmpty(t, sites) assert.NotEmpty(t, sites)