fix bad user info conditional on spo license (#3699)
Primary change is fixing the spo license check in GetUserInfo from a || to a && !. Currently the check returns the spo license error instead of treating it as a drive access condition. Additionally adds a root site check in the m365 isServiceEnabled call, instead of always returning true for sharepoint. This should catch spo license issues on a per-site basis, if someone makes it that far. Finally, adds extra testing (plus some test code consolidation and refactoring) to ensure GetUserInfo is properly tested. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🐛 Bugfix #### Issue(s) * #3671 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
6b8fc57e51
commit
d9f6a9297f
@ -158,13 +158,21 @@ func (ctrl *Controller) IsBackupRunnable(
|
|||||||
resourceOwner string,
|
resourceOwner string,
|
||||||
) (bool, error) {
|
) (bool, error) {
|
||||||
if service == path.SharePointService {
|
if service == path.SharePointService {
|
||||||
// No "enabled" check required for sharepoint
|
_, err := ctrl.AC.Sites().GetRoot(ctx)
|
||||||
|
if err != nil {
|
||||||
|
if clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
|
||||||
|
return false, clues.Stack(graph.ErrServiceNotEnabled, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
info, err := ctrl.AC.Users().GetInfo(ctx, resourceOwner)
|
info, err := ctrl.AC.Users().GetInfo(ctx, resourceOwner)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, clues.Stack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !info.ServiceEnabled(service) {
|
if !info.ServiceEnabled(service) {
|
||||||
@ -208,7 +216,7 @@ func checkServiceEnabled(
|
|||||||
|
|
||||||
info, err := gi.GetInfo(ctx, resource)
|
info, err := gi.GetInfo(ctx, resource)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, false, err
|
return false, false, clues.Stack(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !info.ServiceEnabled(service) {
|
if !info.ServiceEnabled(service) {
|
||||||
|
|||||||
@ -85,7 +85,12 @@ func GetUserInfo(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return client.Users().GetInfo(ctx, userID)
|
aui, err := client.Users().GetInfo(ctx, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, clues.Stack(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aui, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// User fetches a single user's data.
|
// User fetches a single user's data.
|
||||||
|
|||||||
@ -31,7 +31,7 @@ const (
|
|||||||
errorAccessDenied errorCode = "ErrorAccessDenied"
|
errorAccessDenied errorCode = "ErrorAccessDenied"
|
||||||
itemNotFound errorCode = "ErrorItemNotFound"
|
itemNotFound errorCode = "ErrorItemNotFound"
|
||||||
itemNotFoundShort errorCode = "itemNotFound"
|
itemNotFoundShort errorCode = "itemNotFound"
|
||||||
mailboxNotEnabledForRESTAPI errorCode = "MailboxNotEnabledForRESTAPI"
|
MailboxNotEnabledForRESTAPI errorCode = "MailboxNotEnabledForRESTAPI"
|
||||||
malwareDetected errorCode = "malwareDetected"
|
malwareDetected errorCode = "malwareDetected"
|
||||||
// nameAlreadyExists occurs when a request with
|
// nameAlreadyExists occurs when a request with
|
||||||
// @microsoft.graph.conflictBehavior=fail finds a conflicting file.
|
// @microsoft.graph.conflictBehavior=fail finds a conflicting file.
|
||||||
@ -39,7 +39,7 @@ const (
|
|||||||
quotaExceeded errorCode = "ErrorQuotaExceeded"
|
quotaExceeded errorCode = "ErrorQuotaExceeded"
|
||||||
RequestResourceNotFound errorCode = "Request_ResourceNotFound"
|
RequestResourceNotFound errorCode = "Request_ResourceNotFound"
|
||||||
// Returned when we try to get the inbox of a user that doesn't exist.
|
// Returned when we try to get the inbox of a user that doesn't exist.
|
||||||
resourceNotFound errorCode = "ResourceNotFound"
|
ResourceNotFound errorCode = "ResourceNotFound"
|
||||||
// Some datacenters are returning this when we try to get the inbox of a user
|
// Some datacenters are returning this when we try to get the inbox of a user
|
||||||
// that doesn't exist.
|
// that doesn't exist.
|
||||||
invalidUser errorCode = "ErrorInvalidUser"
|
invalidUser errorCode = "ErrorInvalidUser"
|
||||||
@ -131,7 +131,7 @@ func IsErrQuotaExceeded(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsErrExchangeMailFolderNotFound(err error) bool {
|
func IsErrExchangeMailFolderNotFound(err error) bool {
|
||||||
return hasErrorCode(err, resourceNotFound, mailboxNotEnabledForRESTAPI)
|
return hasErrorCode(err, ResourceNotFound, MailboxNotEnabledForRESTAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsErrUserNotFound(err error) bool {
|
func IsErrUserNotFound(err error) bool {
|
||||||
@ -139,7 +139,7 @@ func IsErrUserNotFound(err error) bool {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if hasErrorCode(err, resourceNotFound) {
|
if hasErrorCode(err, ResourceNotFound) {
|
||||||
var odErr odataerrors.ODataErrorable
|
var odErr odataerrors.ODataErrorable
|
||||||
if !errors.As(err, &odErr) {
|
if !errors.As(err, &odErr) {
|
||||||
return false
|
return false
|
||||||
@ -154,7 +154,7 @@ func IsErrUserNotFound(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsErrResourceNotFound(err error) bool {
|
func IsErrResourceNotFound(err error) bool {
|
||||||
return hasErrorCode(err, resourceNotFound)
|
return hasErrorCode(err, ResourceNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsErrCannotOpenFileAttachment(err error) bool {
|
func IsErrCannotOpenFileAttachment(err error) bool {
|
||||||
|
|||||||
@ -235,7 +235,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrUserNotFound() {
|
|||||||
{
|
{
|
||||||
name: "non-matching resource not found",
|
name: "non-matching resource not found",
|
||||||
err: func() error {
|
err: func() error {
|
||||||
res := odErr(string(resourceNotFound))
|
res := odErr(string(ResourceNotFound))
|
||||||
res.GetError().SetMessage(ptr.To("Calendar not found"))
|
res.GetError().SetMessage(ptr.To("Calendar not found"))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
@ -255,7 +255,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrUserNotFound() {
|
|||||||
{
|
{
|
||||||
name: "resource not found oDataErr",
|
name: "resource not found oDataErr",
|
||||||
err: func() error {
|
err: func() error {
|
||||||
res := odErr(string(resourceNotFound))
|
res := odErr(string(ResourceNotFound))
|
||||||
res.GetError().SetMessage(ptr.To("User not found"))
|
res.GetError().SetMessage(ptr.To("User not found"))
|
||||||
|
|
||||||
return res
|
return res
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
package api_test
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/h2non/gock"
|
||||||
|
"github.com/microsoft/kiota-abstractions-go/serialization"
|
||||||
|
kjson "github.com/microsoft/kiota-serialization-json-go"
|
||||||
|
"github.com/microsoftgraph/msgraph-sdk-go/models/odataerrors"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
@ -12,6 +18,59 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/mock"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Intercepting calls with Gock
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
const graphAPIHostURL = "https://graph.microsoft.com"
|
||||||
|
|
||||||
|
func v1APIURLPath(parts ...string) string {
|
||||||
|
return strings.Join(append([]string{"/v1.0"}, parts...), "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
func interceptV1Path(pathParts ...string) *gock.Request {
|
||||||
|
return gock.New(graphAPIHostURL).Get(v1APIURLPath(pathParts...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func odErr(code string) *odataerrors.ODataError {
|
||||||
|
odErr := odataerrors.NewODataError()
|
||||||
|
merr := odataerrors.NewMainError()
|
||||||
|
merr.SetCode(&code)
|
||||||
|
odErr.SetError(merr)
|
||||||
|
|
||||||
|
return odErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func odErrMsg(code, message string) *odataerrors.ODataError {
|
||||||
|
odErr := odataerrors.NewODataError()
|
||||||
|
merr := odataerrors.NewMainError()
|
||||||
|
merr.SetCode(&code)
|
||||||
|
merr.SetMessage(&message)
|
||||||
|
odErr.SetError(merr)
|
||||||
|
|
||||||
|
return odErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseableToMap(t *testing.T, thing serialization.Parsable) map[string]any {
|
||||||
|
sw := kjson.NewJsonSerializationWriter()
|
||||||
|
|
||||||
|
err := sw.WriteObjectValue("", thing)
|
||||||
|
require.NoError(t, err, "serialize")
|
||||||
|
|
||||||
|
content, err := sw.GetSerializedContent()
|
||||||
|
require.NoError(t, err, "serialize")
|
||||||
|
|
||||||
|
var out map[string]any
|
||||||
|
err = json.Unmarshal([]byte(content), &out)
|
||||||
|
require.NoError(t, err, "unmarshall")
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Suite Setup
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
type intgTesterSetup struct {
|
type intgTesterSetup struct {
|
||||||
ac api.Client
|
ac api.Client
|
||||||
gockAC api.Client
|
gockAC api.Client
|
||||||
|
|||||||
@ -1,14 +1,11 @@
|
|||||||
package api_test
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/h2non/gock"
|
"github.com/h2non/gock"
|
||||||
"github.com/microsoft/kiota-abstractions-go/serialization"
|
|
||||||
kjson "github.com/microsoft/kiota-serialization-json-go"
|
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
@ -221,22 +218,6 @@ func (suite *MailAPIIntgSuite) SetupSuite() {
|
|||||||
suite.user = tester.M365UserID(t)
|
suite.user = tester.M365UserID(t)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getJSONObject(t *testing.T, thing serialization.Parsable) map[string]any {
|
|
||||||
sw := kjson.NewJsonSerializationWriter()
|
|
||||||
|
|
||||||
err := sw.WriteObjectValue("", thing)
|
|
||||||
require.NoError(t, err, "serialize")
|
|
||||||
|
|
||||||
content, err := sw.GetSerializedContent()
|
|
||||||
require.NoError(t, err, "serialize")
|
|
||||||
|
|
||||||
var out map[string]any
|
|
||||||
err = json.Unmarshal([]byte(content), &out)
|
|
||||||
require.NoError(t, err, "unmarshall")
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
||||||
mid := "fake-message-id"
|
mid := "fake-message-id"
|
||||||
aid := "fake-attachment-id"
|
aid := "fake-attachment-id"
|
||||||
@ -254,10 +235,9 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
mitem := models.NewMessage()
|
mitem := models.NewMessage()
|
||||||
mitem.SetId(&mid)
|
mitem.SetId(&mid)
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid).
|
||||||
Get("/v1.0/users/user/messages/" + mid).
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), mitem))
|
JSON(parseableToMap(suite.T(), mitem))
|
||||||
},
|
},
|
||||||
expect: assert.NoError,
|
expect: assert.NoError,
|
||||||
},
|
},
|
||||||
@ -268,10 +248,9 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
mitem.SetId(&mid)
|
mitem.SetId(&mid)
|
||||||
mitem.SetHasAttachments(ptr.To(true))
|
mitem.SetHasAttachments(ptr.To(true))
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid).
|
||||||
Get("/v1.0/users/user/messages/" + mid).
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), mitem))
|
JSON(parseableToMap(suite.T(), mitem))
|
||||||
|
|
||||||
atts := models.NewAttachmentCollectionResponse()
|
atts := models.NewAttachmentCollectionResponse()
|
||||||
aitem := models.NewAttachment()
|
aitem := models.NewAttachment()
|
||||||
@ -280,10 +259,9 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
aitem.SetSize(&asize)
|
aitem.SetSize(&asize)
|
||||||
atts.SetValue([]models.Attachmentable{aitem})
|
atts.SetValue([]models.Attachmentable{aitem})
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments").
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments").
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), atts))
|
JSON(parseableToMap(suite.T(), atts))
|
||||||
},
|
},
|
||||||
attachmentCount: 1,
|
attachmentCount: 1,
|
||||||
size: 50,
|
size: 50,
|
||||||
@ -297,10 +275,9 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
mitem.SetId(&mid)
|
mitem.SetId(&mid)
|
||||||
mitem.SetHasAttachments(&truthy)
|
mitem.SetHasAttachments(&truthy)
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid).
|
||||||
Get("/v1.0/users/user/messages/" + mid).
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), mitem))
|
JSON(parseableToMap(suite.T(), mitem))
|
||||||
|
|
||||||
atts := models.NewAttachmentCollectionResponse()
|
atts := models.NewAttachmentCollectionResponse()
|
||||||
aitem := models.NewAttachment()
|
aitem := models.NewAttachment()
|
||||||
@ -311,19 +288,16 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
|
|
||||||
atts.SetValue([]models.Attachmentable{aitem})
|
atts.SetValue([]models.Attachmentable{aitem})
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments").
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments").
|
|
||||||
Reply(503)
|
Reply(503)
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments").
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments").
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), atts))
|
JSON(parseableToMap(suite.T(), atts))
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments", aid).
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments/" + aid).
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), aitem))
|
JSON(parseableToMap(suite.T(), aitem))
|
||||||
},
|
},
|
||||||
attachmentCount: 1,
|
attachmentCount: 1,
|
||||||
size: 200,
|
size: 200,
|
||||||
@ -337,10 +311,9 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
mitem.SetId(&mid)
|
mitem.SetId(&mid)
|
||||||
mitem.SetHasAttachments(&truthy)
|
mitem.SetHasAttachments(&truthy)
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid).
|
||||||
Get("/v1.0/users/user/messages/" + mid).
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), mitem))
|
JSON(parseableToMap(suite.T(), mitem))
|
||||||
|
|
||||||
atts := models.NewAttachmentCollectionResponse()
|
atts := models.NewAttachmentCollectionResponse()
|
||||||
aitem := models.NewAttachment()
|
aitem := models.NewAttachment()
|
||||||
@ -351,20 +324,17 @@ func (suite *MailAPIIntgSuite) TestHugeAttachmentListDownload() {
|
|||||||
|
|
||||||
atts.SetValue([]models.Attachmentable{aitem, aitem, aitem, aitem, aitem})
|
atts.SetValue([]models.Attachmentable{aitem, aitem, aitem, aitem, aitem})
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments").
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments").
|
|
||||||
Reply(503)
|
Reply(503)
|
||||||
|
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments").
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments").
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), atts))
|
JSON(parseableToMap(suite.T(), atts))
|
||||||
|
|
||||||
for i := 0; i < 5; i++ {
|
for i := 0; i < 5; i++ {
|
||||||
gock.New("https://graph.microsoft.com").
|
interceptV1Path("users", "user", "messages", mid, "attachments", aid).
|
||||||
Get("/v1.0/users/user/messages/" + mid + "/attachments/" + aid).
|
|
||||||
Reply(200).
|
Reply(200).
|
||||||
JSON(getJSONObject(suite.T(), aitem))
|
JSON(parseableToMap(suite.T(), aitem))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
attachmentCount: 5,
|
attachmentCount: 5,
|
||||||
|
|||||||
@ -35,21 +35,29 @@ type Sites struct {
|
|||||||
// api calls
|
// api calls
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// GetAll retrieves all sites.
|
func (c Sites) GetRoot(ctx context.Context) (models.Siteable, error) {
|
||||||
func (c Sites) GetAll(ctx context.Context, errs *fault.Bus) ([]models.Siteable, error) {
|
resp, err := c.Stable.
|
||||||
service, err := c.Service()
|
Client().
|
||||||
|
Sites().
|
||||||
|
BySiteId("root").
|
||||||
|
Get(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, graph.Wrap(ctx, err, "getting root site")
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := service.Client().Sites().Get(ctx, nil)
|
return resp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAll retrieves all sites.
|
||||||
|
func (c Sites) GetAll(ctx context.Context, errs *fault.Bus) ([]models.Siteable, error) {
|
||||||
|
resp, err := c.Stable.Client().Sites().Get(ctx, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "getting all sites")
|
return nil, graph.Wrap(ctx, err, "getting all sites")
|
||||||
}
|
}
|
||||||
|
|
||||||
iter, err := msgraphgocore.NewPageIterator[models.Siteable](
|
iter, err := msgraphgocore.NewPageIterator[models.Siteable](
|
||||||
resp,
|
resp,
|
||||||
service.Adapter(),
|
c.Stable.Adapter(),
|
||||||
models.CreateSiteCollectionResponseFromDiscriminatorValue)
|
models.CreateSiteCollectionResponseFromDiscriminatorValue)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, graph.Wrap(ctx, err, "creating sites iterator")
|
return nil, graph.Wrap(ctx, err, "creating sites iterator")
|
||||||
@ -65,8 +73,8 @@ func (c Sites) GetAll(ctx context.Context, errs *fault.Bus) ([]models.Siteable,
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err := validateSite(item)
|
err := ValidateSite(item)
|
||||||
if errors.Is(err, errKnownSkippableCase) {
|
if errors.Is(err, ErrKnownSkippableCase) {
|
||||||
// safe to no-op
|
// safe to no-op
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -182,14 +190,14 @@ func (c Sites) GetDefaultDrive(
|
|||||||
// helpers
|
// helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
var errKnownSkippableCase = clues.New("case is known and skippable")
|
var ErrKnownSkippableCase = clues.New("case is known and skippable")
|
||||||
|
|
||||||
const personalSitePath = "sharepoint.com/personal/"
|
const PersonalSitePath = "sharepoint.com/personal/"
|
||||||
|
|
||||||
// validateSite ensures the item is a Siteable, and contains the necessary
|
// ValidateSite ensures the item is a Siteable, and contains the necessary
|
||||||
// identifiers that we handle with all users.
|
// identifiers that we handle with all users.
|
||||||
// returns the item as a Siteable model.
|
// returns the item as a Siteable model.
|
||||||
func validateSite(item models.Siteable) error {
|
func ValidateSite(item models.Siteable) error {
|
||||||
id := ptr.Val(item.GetId())
|
id := ptr.Val(item.GetId())
|
||||||
if len(id) == 0 {
|
if len(id) == 0 {
|
||||||
return clues.New("missing ID")
|
return clues.New("missing ID")
|
||||||
@ -201,8 +209,8 @@ func validateSite(item models.Siteable) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// personal (ie: oneDrive) sites have to be filtered out server-side.
|
// personal (ie: oneDrive) sites have to be filtered out server-side.
|
||||||
if strings.Contains(wURL, personalSitePath) {
|
if strings.Contains(wURL, PersonalSitePath) {
|
||||||
return clues.Stack(errKnownSkippableCase).
|
return clues.Stack(ErrKnownSkippableCase).
|
||||||
With("site_id", id, "site_web_url", wURL) // TODO: pii
|
With("site_id", id, "site_web_url", wURL) // TODO: pii
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -210,7 +218,7 @@ func validateSite(item models.Siteable) error {
|
|||||||
if len(name) == 0 {
|
if len(name) == 0 {
|
||||||
// the built-in site at "https://{tenant-domain}/search" never has a name.
|
// the built-in site at "https://{tenant-domain}/search" never has a name.
|
||||||
if strings.HasSuffix(wURL, "/search") {
|
if strings.HasSuffix(wURL, "/search") {
|
||||||
return clues.Stack(errKnownSkippableCase).
|
return clues.Stack(ErrKnownSkippableCase).
|
||||||
With("site_id", id, "site_web_url", wURL) // TODO: pii
|
With("site_id", id, "site_web_url", wURL) // TODO: pii
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package api
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
@ -13,8 +13,8 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
"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/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type SitesUnitSuite struct {
|
type SitesUnitSuite struct {
|
||||||
@ -77,7 +77,7 @@ func (suite *SitesUnitSuite) TestValidateSite() {
|
|||||||
args: func() *models.Site {
|
args: func() *models.Site {
|
||||||
s := models.NewSite()
|
s := models.NewSite()
|
||||||
s.SetId(ptr.To("id"))
|
s.SetId(ptr.To("id"))
|
||||||
s.SetWebUrl(ptr.To("https://" + personalSitePath + "/someone's/onedrive"))
|
s.SetWebUrl(ptr.To("https://" + api.PersonalSitePath + "/someone's/onedrive"))
|
||||||
return s
|
return s
|
||||||
}(),
|
}(),
|
||||||
errCheck: assert.Error,
|
errCheck: assert.Error,
|
||||||
@ -93,11 +93,11 @@ func (suite *SitesUnitSuite) TestValidateSite() {
|
|||||||
suite.Run(test.name, func() {
|
suite.Run(test.name, func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
err := validateSite(test.args)
|
err := api.ValidateSite(test.args)
|
||||||
test.errCheck(t, err, clues.ToCore(err))
|
test.errCheck(t, err, clues.ToCore(err))
|
||||||
|
|
||||||
if test.errIsSkippable {
|
if test.errIsSkippable {
|
||||||
assert.ErrorIs(t, err, errKnownSkippableCase)
|
assert.ErrorIs(t, err, api.ErrKnownSkippableCase)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -105,8 +105,7 @@ func (suite *SitesUnitSuite) TestValidateSite() {
|
|||||||
|
|
||||||
type SitesIntgSuite struct {
|
type SitesIntgSuite struct {
|
||||||
tester.Suite
|
tester.Suite
|
||||||
|
its intgTesterSetup
|
||||||
creds account.M365Config
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSitesIntgSuite(t *testing.T) {
|
func TestSitesIntgSuite(t *testing.T) {
|
||||||
@ -118,15 +117,7 @@ func TestSitesIntgSuite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SitesIntgSuite) SetupSuite() {
|
func (suite *SitesIntgSuite) SetupSuite() {
|
||||||
var (
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
t = suite.T()
|
|
||||||
acct = tester.NewM365Account(t)
|
|
||||||
)
|
|
||||||
|
|
||||||
m365, err := acct.M365Config()
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
suite.creds = m365
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *SitesIntgSuite) TestGetAll() {
|
func (suite *SitesIntgSuite) TestGetAll() {
|
||||||
@ -135,15 +126,12 @@ func (suite *SitesIntgSuite) TestGetAll() {
|
|||||||
ctx, flush := tester.NewContext(t)
|
ctx, flush := tester.NewContext(t)
|
||||||
defer flush()
|
defer flush()
|
||||||
|
|
||||||
cli, err := NewClient(suite.creds)
|
sites, err := suite.its.ac.Sites().GetAll(ctx, fault.New(true))
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
sites, err := cli.Sites().GetAll(ctx, fault.New(true))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotZero(t, len(sites), "must have at least one site")
|
require.NotZero(t, len(sites), "must have at least one site")
|
||||||
|
|
||||||
for _, site := range sites {
|
for _, site := range sites {
|
||||||
assert.NotContains(t, ptr.Val(site.GetWebUrl()), personalSitePath, "must not return onedrive sites")
|
assert.NotContains(t, ptr.Val(site.GetWebUrl()), api.PersonalSitePath, "must not return onedrive sites")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,16 +142,9 @@ func (suite *SitesIntgSuite) TestSites_GetByID() {
|
|||||||
host = strings.Split(siteID, ",")[0]
|
host = strings.Split(siteID, ",")[0]
|
||||||
shortID = strings.TrimPrefix(siteID, host+",")
|
shortID = strings.TrimPrefix(siteID, host+",")
|
||||||
siteURL = tester.M365SiteURL(t)
|
siteURL = tester.M365SiteURL(t)
|
||||||
acct = tester.NewM365Account(t)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
creds, err := acct.M365Config()
|
sitesAPI := suite.its.ac.Sites()
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
client, err := NewClient(creds)
|
|
||||||
require.NoError(t, err, clues.ToCore(err))
|
|
||||||
|
|
||||||
sitesAPI := client.Sites()
|
|
||||||
|
|
||||||
table := []struct {
|
table := []struct {
|
||||||
name string
|
name string
|
||||||
@ -191,3 +172,15 @@ func (suite *SitesIntgSuite) TestSites_GetByID() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *SitesIntgSuite) TestGetRoot() {
|
||||||
|
t := suite.T()
|
||||||
|
|
||||||
|
ctx, flush := tester.NewContext(t)
|
||||||
|
defer flush()
|
||||||
|
|
||||||
|
result, err := suite.its.ac.Sites().GetRoot(ctx)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, result, "must find the root site")
|
||||||
|
require.NotEmpty(t, ptr.Val(result.GetId()), "must have an id")
|
||||||
|
}
|
||||||
|
|||||||
@ -95,7 +95,7 @@ func (c Users) GetAll(
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
err := validateUser(item)
|
err := ValidateUser(item)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
el.AddRecoverable(ctx, graph.Wrap(ctx, err, "validating user"))
|
el.AddRecoverable(ctx, graph.Wrap(ctx, err, "validating user"))
|
||||||
} else {
|
} else {
|
||||||
@ -175,17 +175,16 @@ func (c Users) GetInfo(ctx context.Context, userID string) (*UserInfo, error) {
|
|||||||
var (
|
var (
|
||||||
// Assume all services are enabled
|
// Assume all services are enabled
|
||||||
// then filter down to only services the user has enabled
|
// then filter down to only services the user has enabled
|
||||||
userInfo = newUserInfo()
|
userInfo = newUserInfo()
|
||||||
|
|
||||||
mailFolderFound = true
|
mailFolderFound = true
|
||||||
)
|
)
|
||||||
|
|
||||||
// check whether the user is able to access their onedrive drive.
|
// check whether the user is able to access their onedrive drive.
|
||||||
// if they cannot, we can assume they are ineligible for onedrive backups.
|
// if they cannot, we can assume they are ineligible for onedrive backups.
|
||||||
if _, err := c.GetDefaultDrive(ctx, userID); err != nil {
|
if _, err := c.GetDefaultDrive(ctx, userID); err != nil {
|
||||||
if !clues.HasLabel(err, graph.LabelsMysiteNotFound) || clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
|
if !clues.HasLabel(err, graph.LabelsMysiteNotFound) && !clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
|
||||||
logger.CtxErr(ctx, err).Error("getting user's drive")
|
logger.CtxErr(ctx, err).Error("getting user's default drive")
|
||||||
return nil, graph.Wrap(ctx, err, "getting user's drive")
|
return nil, graph.Wrap(ctx, err, "getting user's default drive info")
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Ctx(ctx).Info("resource owner does not have a drive")
|
logger.Ctx(ctx).Info("resource owner does not have a drive")
|
||||||
@ -345,9 +344,9 @@ func (c Users) getFirstInboxMessage(
|
|||||||
// helpers
|
// helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
// validateUser ensures the item is a Userable, and contains the necessary
|
// ValidateUser ensures the item is a Userable, and contains the necessary
|
||||||
// identifiers that we handle with all users.
|
// identifiers that we handle with all users.
|
||||||
func validateUser(item models.Userable) error {
|
func ValidateUser(item models.Userable) error {
|
||||||
if item.GetId() == nil {
|
if item.GetId() == nil {
|
||||||
return clues.New("missing ID")
|
return clues.New("missing ID")
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,14 +1,17 @@
|
|||||||
package api
|
package api_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/h2non/gock"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/m365/graph"
|
||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UsersUnitSuite struct {
|
type UsersUnitSuite struct {
|
||||||
@ -57,8 +60,129 @@ func (suite *UsersUnitSuite) TestValidateUser() {
|
|||||||
suite.Run(tt.name, func() {
|
suite.Run(tt.name, func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
err := validateUser(tt.args)
|
err := api.ValidateUser(tt.args)
|
||||||
tt.errCheck(t, err, clues.ToCore(err))
|
tt.errCheck(t, err, clues.ToCore(err))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UsersIntgSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
its intgTesterSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUsersIntgSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &UsersIntgSuite{
|
||||||
|
Suite: tester.NewIntegrationSuite(
|
||||||
|
t,
|
||||||
|
[][]string{tester.M365AcctCredEnvs}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UsersIntgSuite) SetupSuite() {
|
||||||
|
suite.its = newIntegrationTesterSetup(suite.T())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *UsersIntgSuite) TestUsers_GetInfo_errors() {
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
setGocks func(t *testing.T)
|
||||||
|
expectErr func(t *testing.T, err error)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default drive err - mysite not found",
|
||||||
|
setGocks: func(t *testing.T) {
|
||||||
|
interceptV1Path("users", "user", "drive").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErrMsg("anycode", string(graph.MysiteNotFound))))
|
||||||
|
interceptV1Path("users", "user", "mailFolders", "inbox").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErr(string(graph.ResourceNotFound))))
|
||||||
|
},
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default drive err - no sharepoint license",
|
||||||
|
setGocks: func(t *testing.T) {
|
||||||
|
interceptV1Path("users", "user", "drive").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErrMsg("anycode", string(graph.NoSPLicense))))
|
||||||
|
interceptV1Path("users", "user", "mailFolders", "inbox").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErr(string(graph.ResourceNotFound))))
|
||||||
|
},
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "default drive err - other error",
|
||||||
|
setGocks: func(t *testing.T) {
|
||||||
|
interceptV1Path("users", "user", "drive").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErrMsg("somecode", "somemessage")))
|
||||||
|
},
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.Error(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mail inbox err - user not found",
|
||||||
|
setGocks: func(t *testing.T) {
|
||||||
|
interceptV1Path("users", "user", "drive").
|
||||||
|
Reply(200).
|
||||||
|
JSON(parseableToMap(t, models.NewDrive()))
|
||||||
|
interceptV1Path("users", "user", "mailFolders", "inbox").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErr(string(graph.RequestResourceNotFound))))
|
||||||
|
},
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mail inbox err - user not found",
|
||||||
|
setGocks: func(t *testing.T) {
|
||||||
|
interceptV1Path("users", "user", "drive").
|
||||||
|
Reply(200).
|
||||||
|
JSON(parseableToMap(t, models.NewDrive()))
|
||||||
|
interceptV1Path("users", "user", "mailFolders", "inbox").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErr(string(graph.MailboxNotEnabledForRESTAPI))))
|
||||||
|
},
|
||||||
|
expectErr: func(t *testing.T, err error) {
|
||||||
|
assert.NoError(t, err, clues.ToCore(err))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mail inbox err - other error",
|
||||||
|
setGocks: func(t *testing.T) {
|
||||||
|
interceptV1Path("users", "user", "drive").
|
||||||
|
Reply(200).
|
||||||
|
JSON(parseableToMap(t, models.NewDrive()))
|
||||||
|
interceptV1Path("users", "user", "mailFolders", "inbox").
|
||||||
|
Reply(400).
|
||||||
|
JSON(parseableToMap(t, odErrMsg("somecode", "somemessage")))
|
||||||
|
},
|
||||||
|
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()
|
||||||
|
defer gock.Off()
|
||||||
|
|
||||||
|
test.setGocks(t)
|
||||||
|
|
||||||
|
_, err := suite.its.gockAC.Users().GetInfo(ctx, "user")
|
||||||
|
test.expectErr(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user