teams discovery API (#3850)

<!-- PR description-->

Teams discovery API
- fetch all groups from endpoint and filter teams out of it.
- fetch team with ID. 

Permission added- 
- Application- Team.ReadBasic.All
- Group.Read.All

#### Does this PR need a docs update or release note?
- [ ] 🕐 Yes, but in a later PR

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature

#### 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/3836

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [ ] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
neha_gupta 2023-08-16 11:32:37 +05:30 committed by GitHub
parent 5389cdf058
commit bebaf3b462
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 518 additions and 8 deletions

View File

@ -1,4 +1,4 @@
// Code generated by "stringer -type=opStatus -linecomment"; DO NOT EDIT. // Code generated by "stringer -type=OpStatus -linecomment"; DO NOT EDIT.
package operations package operations
@ -15,13 +15,13 @@ func _() {
_ = x[NoData-4] _ = x[NoData-4]
} }
const _opStatus_name = "Status UnknownIn ProgressCompletedFailedNo Data" const _OpStatus_name = "Status UnknownIn ProgressCompletedFailedNo Data"
var _opStatus_index = [...]uint8{0, 14, 25, 34, 40, 47} var _OpStatus_index = [...]uint8{0, 14, 25, 34, 40, 47}
func (i OpStatus) String() string { func (i OpStatus) String() string {
if i < 0 || i >= OpStatus(len(_opStatus_index)-1) { if i < 0 || i >= OpStatus(len(_OpStatus_index)-1) {
return "opStatus(" + strconv.FormatInt(int64(i), 10) + ")" return "OpStatus(" + strconv.FormatInt(int64(i), 10) + ")"
} }
return _opStatus_name[_opStatus_index[i]:_opStatus_index[i+1]] return _OpStatus_name[_OpStatus_index[i]:_OpStatus_index[i+1]]
} }

View File

@ -26,6 +26,8 @@ const (
TestCfgSecondarySiteID = "secondarym365siteid" TestCfgSecondarySiteID = "secondarym365siteid"
TestCfgSiteID = "m365siteid" TestCfgSiteID = "m365siteid"
TestCfgSiteURL = "m365siteurl" TestCfgSiteURL = "m365siteurl"
TestCfgTeamID = "m365teamid"
TestCfgGroupID = "m365groupid"
TestCfgUserID = "m365userid" TestCfgUserID = "m365userid"
TestCfgSecondaryUserID = "secondarym365userid" TestCfgSecondaryUserID = "secondarym365userid"
TestCfgTertiaryUserID = "tertiarym365userid" TestCfgTertiaryUserID = "tertiarym365userid"
@ -41,6 +43,8 @@ const (
EnvCorsoM365LoadTestOrgUsers = "CORSO_M365_LOAD_TEST_ORG_USERS" EnvCorsoM365LoadTestOrgUsers = "CORSO_M365_LOAD_TEST_ORG_USERS"
EnvCorsoM365TestSiteID = "CORSO_M365_TEST_SITE_ID" EnvCorsoM365TestSiteID = "CORSO_M365_TEST_SITE_ID"
EnvCorsoM365TestSiteURL = "CORSO_M365_TEST_SITE_URL" EnvCorsoM365TestSiteURL = "CORSO_M365_TEST_SITE_URL"
EnvCorsoM365TestTeamID = "CORSO_M365_TEST_TEAM_ID"
EnvCorsoM365TestGroupID = "CORSO_M365_TEST_GROUP_ID"
EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID" EnvCorsoM365TestUserID = "CORSO_M365_TEST_USER_ID"
EnvCorsoSecondaryM365TestSiteID = "CORSO_SECONDARY_M365_TEST_SITE_ID" EnvCorsoSecondaryM365TestSiteID = "CORSO_SECONDARY_M365_TEST_SITE_ID"
EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID" EnvCorsoSecondaryM365TestUserID = "CORSO_SECONDARY_M365_TEST_USER_ID"
@ -150,6 +154,18 @@ func ReadTestConfig() (map[string]string, error) {
os.Getenv(EnvCorsoM365TestSiteID), os.Getenv(EnvCorsoM365TestSiteID),
vpr.GetString(TestCfgSiteID), vpr.GetString(TestCfgSiteID),
"4892edf5-2ebf-46be-a6e5-a40b2cbf1c1a,38ab6d06-fc82-4417-af93-22d8733c22be") "4892edf5-2ebf-46be-a6e5-a40b2cbf1c1a,38ab6d06-fc82-4417-af93-22d8733c22be")
fallbackTo(
testEnv,
TestCfgTeamID,
os.Getenv(EnvCorsoM365TestTeamID),
vpr.GetString(TestCfgTeamID),
"6f24b40d-b13d-4752-980f-f5fb9fba7aa0")
fallbackTo(
testEnv,
TestCfgGroupID,
os.Getenv(EnvCorsoM365TestGroupID),
vpr.GetString(TestCfgGroupID),
"6f24b40d-b13d-4752-980f-f5fb9fba7aa0")
fallbackTo( fallbackTo(
testEnv, testEnv,
TestCfgSiteURL, TestCfgSiteURL,

View File

@ -220,3 +220,29 @@ func UnlicensedM365UserID(t *testing.T) string {
return strings.ToLower(cfg[TestCfgSecondaryUserID]) return strings.ToLower(cfg[TestCfgSecondaryUserID])
} }
// Teams
// M365TeamsID returns a teamID string representing the m365TeamsID described
// by either the env var CORSO_M365_TEST_TEAM_ID, the corso_test.toml config
// file or the default value (in that order of priority). The default is a
// last-attempt fallback that will only work on alcion's testing org.
func M365TeamsID(t *testing.T) string {
cfg, err := ReadTestConfig()
require.NoError(t, err, "retrieving m365 team id from test configuration: %+v", clues.ToCore(err))
return strings.ToLower(cfg[TestCfgTeamID])
}
// Groups
// M365GroupID returns a groupID string representing the m365GroupID described
// by either the env var CORSO_M365_TEST_GROUP_ID, the corso_test.toml config
// file or the default value (in that order of priority). The default is a
// last-attempt fallback that will only work on alcion's testing org.
func M365GroupID(t *testing.T) string {
cfg, err := ReadTestConfig()
require.NoError(t, err, "retrieving m365 group id from test configuration: %+v", clues.ToCore(err))
return strings.ToLower(cfg[TestCfgTeamID])
}

View File

@ -29,6 +29,8 @@ const (
ExchangeMetadataService // exchangeMetadata ExchangeMetadataService // exchangeMetadata
OneDriveMetadataService // onedriveMetadata OneDriveMetadataService // onedriveMetadata
SharePointMetadataService // sharepointMetadata SharePointMetadataService // sharepointMetadata
GroupsService // groups
GroupsMetadataService // groupsMetadata
) )
func toServiceType(service string) ServiceType { func toServiceType(service string) ServiceType {

View File

@ -15,11 +15,13 @@ func _() {
_ = x[ExchangeMetadataService-4] _ = x[ExchangeMetadataService-4]
_ = x[OneDriveMetadataService-5] _ = x[OneDriveMetadataService-5]
_ = x[SharePointMetadataService-6] _ = x[SharePointMetadataService-6]
_ = x[GroupsService-7]
_ = x[GroupsMetadataService-8]
} }
const _ServiceType_name = "UnknownServiceexchangeonedrivesharepointexchangeMetadataonedriveMetadatasharepointMetadata" const _ServiceType_name = "UnknownServiceexchangeonedrivesharepointexchangeMetadataonedriveMetadatasharepointMetadatagroupsgroupsMetadata"
var _ServiceType_index = [...]uint8{0, 14, 22, 30, 40, 56, 72, 90} var _ServiceType_index = [...]uint8{0, 14, 22, 30, 40, 56, 72, 90, 96, 110}
func (i ServiceType) String() string { func (i ServiceType) String() string {
if i < 0 || i >= ServiceType(len(_ServiceType_index)-1) { if i < 0 || i >= ServiceType(len(_ServiceType_index)-1) {

View File

@ -0,0 +1,205 @@
package api
import (
"context"
"github.com/alcionai/clues"
msgraphgocore "github.com/microsoftgraph/msgraph-sdk-go-core"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/alcionai/corso/src/internal/common/str"
"github.com/alcionai/corso/src/internal/common/tform"
"github.com/alcionai/corso/src/internal/m365/graph"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/logger"
)
const (
teamsAdditionalDataLabel = "Team"
ResourceProvisioningOptions = "resourceProvisioningOptions"
)
// ---------------------------------------------------------------------------
// controller
// ---------------------------------------------------------------------------
func (c Client) Groups() Groups {
return Groups{c}
}
// On creation of each Teams team a corrsponding 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.
// Groups is an interface-compliant provider of the client.
type Groups struct {
Client
}
// GetAllGroups retrieves all groups.
func (c Groups) GetAll(
ctx context.Context,
errs *fault.Bus,
) ([]models.Groupable, error) {
service, err := c.Service()
if err != nil {
return nil, err
}
return getGroups(ctx, errs, service)
}
// GetTeams retrieves all Teams.
func (c Groups) GetTeams(
ctx context.Context,
errs *fault.Bus,
) ([]models.Groupable, error) {
service, err := c.Service()
if err != nil {
return nil, err
}
groups, err := getGroups(ctx, errs, service)
if err != nil {
return nil, err
}
return OnlyTeams(ctx, groups), nil
}
// GetAll retrieves all groups.
func getGroups(
ctx context.Context,
errs *fault.Bus,
service graph.Servicer,
) ([]models.Groupable, error) {
resp, err := service.Client().Groups().Get(ctx, nil)
if err != nil {
return nil, graph.Wrap(ctx, err, "getting all groups")
}
iter, err := msgraphgocore.NewPageIterator[models.Groupable](
resp,
service.Adapter(),
models.CreateGroupCollectionResponseFromDiscriminatorValue)
if err != nil {
return nil, graph.Wrap(ctx, err, "creating groups iterator")
}
var (
groups = make([]models.Groupable, 0)
el = errs.Local()
)
iterator := func(item models.Groupable) bool {
if el.Failure() != nil {
return false
}
err := ValidateGroup(item)
if err != nil {
el.AddRecoverable(ctx, graph.Wrap(ctx, err, "validating groups"))
} else {
groups = append(groups, item)
}
return true
}
if err := iter.Iterate(ctx, iterator); err != nil {
return nil, graph.Wrap(ctx, err, "iterating all groups")
}
return groups, el.Failure()
}
func OnlyTeams(ctx context.Context, groups []models.Groupable) []models.Groupable {
log := logger.Ctx(ctx)
var teams []models.Groupable
for _, g := range groups {
if g.GetAdditionalData()[ResourceProvisioningOptions] != nil {
val, _ := tform.AnyValueToT[[]any](ResourceProvisioningOptions, g.GetAdditionalData())
for _, v := range val {
s, err := str.AnyToString(v)
if err != nil {
log.Debug("could not be converted to string value: ", ResourceProvisioningOptions)
continue
}
if s == teamsAdditionalDataLabel {
teams = append(teams, g)
}
}
}
}
return teams
}
// GetID retrieves group by groupID.
func (c Groups) GetByID(
ctx context.Context,
identifier string,
) (models.Groupable, error) {
service, err := c.Service()
if err != nil {
return nil, err
}
resp, err := service.Client().Groups().ByGroupId(identifier).Get(ctx, nil)
if err != nil {
err := graph.Wrap(ctx, err, "getting group by id")
return nil, err
}
return resp, graph.Stack(ctx, err).OrNil()
}
// GetTeamByID retrieves group by groupID.
func (c Groups) GetTeamByID(
ctx context.Context,
identifier string,
) (models.Groupable, error) {
service, err := c.Service()
if err != nil {
return nil, err
}
resp, err := service.Client().Groups().ByGroupId(identifier).Get(ctx, nil)
if err != nil {
err := graph.Wrap(ctx, err, "getting group by id")
return nil, err
}
groups := []models.Groupable{resp}
if len(OnlyTeams(ctx, groups)) == 0 {
err := clues.New("given teamID is not related to any team")
return nil, err
}
return resp, graph.Stack(ctx, err).OrNil()
}
// ---------------------------------------------------------------------------
// helpers
// ---------------------------------------------------------------------------
// ValidateGroup ensures the item is a Groupable, and contains the necessary
// identifiers that we handle with all groups.
func ValidateGroup(item models.Groupable) error {
if item.GetId() == nil {
return clues.New("missing ID")
}
if item.GetDisplayName() == nil {
return clues.New("missing display name")
}
return nil
}

View File

@ -0,0 +1,250 @@
package api_test
import (
"testing"
"github.com/alcionai/clues"
"github.com/google/uuid"
"github.com/microsoftgraph/msgraph-sdk-go/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/alcionai/corso/src/internal/common/ptr"
"github.com/alcionai/corso/src/internal/tester"
"github.com/alcionai/corso/src/internal/tester/tconfig"
"github.com/alcionai/corso/src/pkg/fault"
"github.com/alcionai/corso/src/pkg/services/m365/api"
)
type GroupUnitSuite struct {
tester.Suite
}
func TestGroupsUnitSuite(t *testing.T) {
suite.Run(t, &GroupUnitSuite{Suite: tester.NewUnitSuite(t)})
}
func (suite *GroupUnitSuite) TestValidateGroup() {
group := models.NewGroup()
group.SetDisplayName(ptr.To("testgroup"))
group.SetId(ptr.To("testID"))
tests := []struct {
name string
args models.Groupable
errCheck assert.ErrorAssertionFunc
errIsSkippable bool
}{
{
name: "Valid group ",
args: func() *models.Group {
s := models.NewGroup()
s.SetId(ptr.To("id"))
s.SetDisplayName(ptr.To("testgroup"))
return s
}(),
errCheck: assert.NoError,
},
{
name: "No name",
args: func() *models.Group {
s := models.NewGroup()
s.SetId(ptr.To("id"))
return s
}(),
errCheck: assert.Error,
},
{
name: "No ID",
args: func() *models.Group {
s := models.NewGroup()
s.SetDisplayName(ptr.To("testgroup"))
return s
}(),
errCheck: assert.Error,
},
}
for _, test := range tests {
suite.Run(test.name, func() {
t := suite.T()
err := api.ValidateGroup(test.args)
test.errCheck(t, err, clues.ToCore(err))
if test.errIsSkippable {
assert.ErrorIs(t, err, api.ErrKnownSkippableCase)
}
})
}
}
type GroupsIntgSuite struct {
tester.Suite
its intgTesterSetup
}
func TestGroupsIntgSuite(t *testing.T) {
suite.Run(t, &GroupsIntgSuite{
Suite: tester.NewIntegrationSuite(
t,
[][]string{tconfig.M365AcctCredEnvs}),
})
}
func (suite *GroupsIntgSuite) SetupSuite() {
suite.its = newIntegrationTesterSetup(suite.T())
}
func (suite *GroupsIntgSuite) TestGetAllGroups() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
groups, err := suite.its.ac.
Groups().
GetAll(ctx, fault.New(true))
require.NoError(t, err)
require.NotZero(t, len(groups), "must have at least one group")
}
func (suite *GroupsIntgSuite) TestGetAllTeams() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
teams, err := suite.its.ac.
Groups().
GetTeams(ctx, fault.New(true))
require.NoError(t, err)
require.NotZero(t, len(teams), "must have at least one teams")
groups, err := suite.its.ac.
Groups().
GetAll(ctx, fault.New(true))
require.NoError(t, err)
require.NotZero(t, len(groups), "must have at least one group")
var isTeam bool
if len(groups) > len(teams) {
isTeam = true
}
assert.True(t, isTeam, "must only return teams")
}
func (suite *GroupsIntgSuite) TestTeams_GetByID() {
var (
t = suite.T()
teamID = tconfig.M365TeamsID(t)
)
teamsAPI := suite.its.ac.Groups()
table := []struct {
name string
id string
expectErr func(*testing.T, error)
}{
{
name: "3 part id",
id: teamID,
expectErr: func(t *testing.T, err error) {
assert.NoError(t, err, clues.ToCore(err))
},
},
{
name: "malformed 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() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
_, err := teamsAPI.GetTeamByID(ctx, test.id)
test.expectErr(t, err)
})
}
}
func (suite *GroupsIntgSuite) TestGroups_GetByID() {
var (
t = suite.T()
groupID = tconfig.M365GroupID(t)
)
groupsAPI := suite.its.ac.Groups()
table := []struct {
name string
id string
expectErr func(*testing.T, error)
}{
{
name: "3 part id",
id: groupID,
expectErr: func(t *testing.T, err error) {
assert.NoError(t, err, clues.ToCore(err))
},
},
{
name: "malformed 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() {
t := suite.T()
ctx, flush := tester.NewContext(t)
defer flush()
_, err := groupsAPI.GetByID(ctx, test.id)
test.expectErr(t, err)
})
}
}

View File

@ -83,6 +83,7 @@ type intgTesterSetup struct {
siteID string siteID string
siteDriveID string siteDriveID string
siteDriveRootFolderID string siteDriveRootFolderID string
teamID string
} }
func newIntegrationTesterSetup(t *testing.T) intgTesterSetup { func newIntegrationTesterSetup(t *testing.T) intgTesterSetup {
@ -131,5 +132,13 @@ func newIntegrationTesterSetup(t *testing.T) intgTesterSetup {
its.siteDriveRootFolderID = ptr.Val(siteDriveRootFolder.GetId()) its.siteDriveRootFolderID = ptr.Val(siteDriveRootFolder.GetId())
// teams
its.teamID = tconfig.M365TeamsID(t)
team, err := its.ac.Groups().GetTeamByID(ctx, its.teamID)
require.NoError(t, err, clues.ToCore(err))
its.teamID = ptr.Val(team.GetId())
return its return its
} }