introduce errs/core (#4897)
first step towards having a centralized set of error sentinels that can be passed around corso instead of re-using low level error sentinels like those found in the graph/errors.go file. This PR works as a standalone, and only handles the lowest hanging fruit. SDK consumers will need to change their error enum references, but all api behavior should remain the same. --- #### Does this PR need a docs update or release note? - [x] ⛔ No #### Type of change - [x] 🤖 Supportability/Tests #### Issue(s) * #4685 #### Test Plan - [x] ⚡ Unit test - [x] 💚 E2E
This commit is contained in:
parent
be6cb13a7b
commit
971d874462
@ -20,11 +20,11 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/backup"
|
"github.com/alcionai/corso/src/pkg/backup"
|
||||||
"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/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"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/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/selectors"
|
"github.com/alcionai/corso/src/pkg/selectors"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
|
||||||
"github.com/alcionai/corso/src/pkg/store"
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -210,7 +210,7 @@ func genericCreateCommand(
|
|||||||
|
|
||||||
err = bo.Run(ictx)
|
err = bo.Run(ictx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, graph.ErrServiceNotEnabled) {
|
if errors.Is(err, core.ErrServiceNotEnabled) {
|
||||||
logger.Ctx(ctx).Infow("service not enabled",
|
logger.Ctx(ctx).Infow("service not enabled",
|
||||||
"resource_owner_id", bo.ResourceOwner.ID(),
|
"resource_owner_id", bo.ResourceOwner.ID(),
|
||||||
"service", serviceName)
|
"service", serviceName)
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
bupMD "github.com/alcionai/corso/src/pkg/backup/metadata"
|
||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/filters"
|
"github.com/alcionai/corso/src/pkg/filters"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -170,7 +171,7 @@ func verifyBackupInputs(sels selectors.Selector, cachedIDs []string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !filters.Contains(ids).Compare(sels.ID()) {
|
if !filters.Contains(ids).Compare(sels.ID()) {
|
||||||
return clues.Stack(graph.ErrResourceOwnerNotFound).
|
return clues.Stack(core.ErrResourceOwnerNotFound).
|
||||||
With("selector_protected_resource", sels.DiscreteOwner)
|
With("selector_protected_resource", sels.DiscreteOwner)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -16,6 +16,7 @@ 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/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
@ -255,7 +256,7 @@ func (r resourceGetter) GetResourceIDAndNameFrom(
|
|||||||
id, name, err = r.getter.GetIDAndName(ctx, owner, api.CallConfig{})
|
id, name, err = r.getter.GetIDAndName(ctx, owner, api.CallConfig{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if graph.IsErrUserNotFound(err) {
|
if graph.IsErrUserNotFound(err) {
|
||||||
return nil, clues.Stack(graph.ErrResourceOwnerNotFound, err)
|
return nil, clues.Stack(core.ErrResourceOwnerNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.IsErrResourceLocked(err) {
|
if graph.IsErrResourceLocked(err) {
|
||||||
@ -266,7 +267,7 @@ func (r resourceGetter) GetResourceIDAndNameFrom(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if len(id) == 0 || len(name) == 0 {
|
if len(id) == 0 || len(name) == 0 {
|
||||||
return nil, clues.Stack(graph.ErrResourceOwnerNotFound)
|
return nil, clues.Stack(core.ErrResourceOwnerNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
return idname.NewProvider(id, name), nil
|
return idname.NewProvider(id, name), nil
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"github.com/alcionai/clues"
|
"github.com/alcionai/clues"
|
||||||
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
"github.com/microsoftgraph/msgraph-sdk-go/models"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -27,7 +28,7 @@ func IsServiceEnabled(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if graph.IsErrUserNotFound(err) {
|
if graph.IsErrUserNotFound(err) {
|
||||||
return false, clues.Stack(graph.ErrResourceOwnerNotFound, err)
|
return false, clues.Stack(core.ErrResourceOwnerNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.IsErrResourceLocked(err) {
|
if graph.IsErrResourceLocked(err) {
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
"github.com/alcionai/corso/src/pkg/dttm"
|
"github.com/alcionai/corso/src/pkg/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"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/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -231,7 +232,7 @@ func (op *BackupOperation) Run(ctx context.Context) (err error) {
|
|||||||
|
|
||||||
if !enabled {
|
if !enabled {
|
||||||
// Return named error so that we can check for it in caller.
|
// Return named error so that we can check for it in caller.
|
||||||
err = clues.Wrap(graph.ErrServiceNotEnabled, "service not enabled for backup")
|
err = clues.Stack(core.ErrServiceNotEnabled)
|
||||||
op.Errors.Fail(err)
|
op.Errors.Fail(err)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -27,11 +27,11 @@ import (
|
|||||||
"github.com/alcionai/corso/src/pkg/control"
|
"github.com/alcionai/corso/src/pkg/control"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
"github.com/alcionai/corso/src/pkg/dttm"
|
"github.com/alcionai/corso/src/pkg/dttm"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"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/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"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
|
||||||
"github.com/alcionai/corso/src/pkg/store"
|
"github.com/alcionai/corso/src/pkg/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -246,7 +246,7 @@ func (op *RestoreOperation) do(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !enabled {
|
if !enabled {
|
||||||
return nil, clues.WrapWC(ctx, graph.ErrServiceNotEnabled, "service not enabled for restore")
|
return nil, clues.StackWC(ctx, core.ErrServiceNotEnabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
pcfg := observe.ProgressCfg{
|
pcfg := observe.ProgressCfg{
|
||||||
|
|||||||
106
src/pkg/errs/core/core.go
Normal file
106
src/pkg/errs/core/core.go
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import "errors"
|
||||||
|
|
||||||
|
// -----------------------------------------------------------------------------------------------
|
||||||
|
// core.Err sentinels are provided to maintain a reference of commonplace errors throughout Corso.
|
||||||
|
//
|
||||||
|
// The general idea is that these errors allow the repo (and consumers of its CLI and SDK apis)
|
||||||
|
// to communicate clearly about the central identity of an error (ie: its "core"), without leaking
|
||||||
|
// service-specific details and imports from low-level apis, clients, and other packages.
|
||||||
|
//
|
||||||
|
// In order to maintain sanity here, a couple rules should be followed.
|
||||||
|
//
|
||||||
|
// 1. Sentinels should have generic messages. No references to downstream concepts.
|
||||||
|
// Basic cleanliness here. Downstream references contaminate the sentinel purpose.
|
||||||
|
//
|
||||||
|
// 2. Maintain coarseness.
|
||||||
|
// We won't need a core.Err version of every lower-level error. Try, where possible,
|
||||||
|
// to group concepts into broad categories. Ex: prefer "resource not found" over
|
||||||
|
// "user not found" or "site not found".
|
||||||
|
//
|
||||||
|
// 3. Always Stack/Wrap core.Errs. Only once.
|
||||||
|
// `return core.ErrFoo` should be avoided. Also, if you're handling a error returned
|
||||||
|
// by some internal package, do your due diligence and make sure it isn't already
|
||||||
|
// identified by a core.Err at a lower level.
|
||||||
|
//
|
||||||
|
// 4. Stacking/Wrapping is the lowest layer's job.
|
||||||
|
// We prefer to returning sentinels at lower layers instead of parsing errors at
|
||||||
|
// higher layers. This ensures higher layers only need to run errors.Is and .As
|
||||||
|
// checks, without needing take on low-level error details.
|
||||||
|
//
|
||||||
|
// 5. Add comments to explain the sentinels.
|
||||||
|
// Future maintainers may not easily grok the intent behind an existing sentinel.
|
||||||
|
// Because we want to keep the error messages themselves small and clean, a short
|
||||||
|
// explanation in the comments, even a basic one, can help a lot.
|
||||||
|
//
|
||||||
|
// 6. This package gets more important at higher layers.
|
||||||
|
// The goal is to make life easier for layers that are the most detached from low-
|
||||||
|
// level and internal packages. The closer that code gets to those lower layers,
|
||||||
|
// the less important it is to strictly use this package. But since most errors
|
||||||
|
// bubble up to the SDK and CLI APIs, it is eventually a critical issue that we
|
||||||
|
// categorize our errors smartly for those end users.
|
||||||
|
// -----------------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
type Err struct {
|
||||||
|
msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Err) Error() string {
|
||||||
|
return e.msg
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
// currently we have no internal throttling controls. We only try to match
|
||||||
|
// external throttling requirements. This sentinel assumes that an external
|
||||||
|
// server has returned one or more throttling errors which has stopped
|
||||||
|
// operation progress.
|
||||||
|
ErrApplicationThrottled = &Err{msg: "application throttled"}
|
||||||
|
// about what it sounds like: we tried to look for a backup by ID, but the
|
||||||
|
// storage layer couldn't find anything for that ID.
|
||||||
|
ErrBackupNotFound = &Err{msg: "backup not found"}
|
||||||
|
// a catch-all for downstream api auth issues. doesn't matter which api.
|
||||||
|
ErrInsufficientAuthorization = &Err{msg: "insufficient authorization"}
|
||||||
|
// specifically for repository creation: if we tried to create a repo and
|
||||||
|
// it already exists with those credentials, we return this error.
|
||||||
|
ErrRepoAlreadyExists = &Err{msg: "repository already exists"}
|
||||||
|
// use this when a resource (user, etc; whatever owner is used to own the
|
||||||
|
// data in the given backup) is unable to be used for backup or restore.
|
||||||
|
// some nuance here: this is not the same as a broad-scale auth issue.
|
||||||
|
// it is also not the same as a "not found" issue. it's specific to
|
||||||
|
// cases where we can find the resource, and have authorization to access
|
||||||
|
// it, but are told by the external system that the resource is somehow
|
||||||
|
// unusable.
|
||||||
|
ErrResourceNotAccessible = &Err{msg: "resource not accesible"}
|
||||||
|
// use this when a resource (user, etc; whatever owner is used to own the
|
||||||
|
// data in the given backup) cannot be found in the system by the ID that
|
||||||
|
// the end user provided.
|
||||||
|
ErrResourceOwnerNotFound = &Err{msg: "resource owner not found"}
|
||||||
|
// a service is the set of application data within a given provider. eg:
|
||||||
|
// if m365 is the provider, then exchange is a service, so is oneDrive.
|
||||||
|
// this sentinel is used to indicate that the service in question is not
|
||||||
|
// accessible to the user. this is not the same as an auth error. more
|
||||||
|
// often its a license issue. as in: the tenant hasn't purchased the use
|
||||||
|
// of this service (but may have purchased the use of other services in
|
||||||
|
// the same provider).
|
||||||
|
ErrServiceNotEnabled = &Err{msg: "service not enabled"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// As is a quality-of-life wrapper around errors.As, to retrieve the core.Err
|
||||||
|
// out of any arbitrary error.
|
||||||
|
func As(err error) (*Err, bool) {
|
||||||
|
if err == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ce *Err
|
||||||
|
ok = errors.As(err, &ce)
|
||||||
|
)
|
||||||
|
|
||||||
|
if !ok {
|
||||||
|
return nil, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
return ce, ok
|
||||||
|
}
|
||||||
91
src/pkg/errs/core/core_test.go
Normal file
91
src/pkg/errs/core/core_test.go
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
package core_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/alcionai/clues"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/suite"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ErrUnitSuite struct {
|
||||||
|
tester.Suite
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestErrUnitSuite(t *testing.T) {
|
||||||
|
suite.Run(t, &ErrUnitSuite{Suite: tester.NewUnitSuite(t)})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *ErrUnitSuite) TestAs() {
|
||||||
|
// shorthand reference for ease of reading
|
||||||
|
cErr := core.ErrApplicationThrottled
|
||||||
|
adHoc := &core.Err{}
|
||||||
|
|
||||||
|
table := []struct {
|
||||||
|
name string
|
||||||
|
err error
|
||||||
|
expectOK assert.BoolAssertionFunc
|
||||||
|
expectErr func(t *testing.T, ce *core.Err)
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil",
|
||||||
|
err: nil,
|
||||||
|
expectOK: assert.False,
|
||||||
|
expectErr: func(t *testing.T, ce *core.Err) {
|
||||||
|
assert.Nil(t, ce)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "non-matching",
|
||||||
|
err: assert.AnError,
|
||||||
|
expectOK: assert.False,
|
||||||
|
expectErr: func(t *testing.T, ce *core.Err) {
|
||||||
|
assert.Nil(t, ce)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "matching",
|
||||||
|
err: cErr,
|
||||||
|
expectOK: assert.True,
|
||||||
|
expectErr: func(t *testing.T, ce *core.Err) {
|
||||||
|
assert.Equal(t, cErr, ce)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "adHoc",
|
||||||
|
err: adHoc,
|
||||||
|
expectOK: assert.True,
|
||||||
|
expectErr: func(t *testing.T, ce *core.Err) {
|
||||||
|
assert.Equal(t, adHoc, ce)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "stacked",
|
||||||
|
err: clues.Stack(assert.AnError, cErr, assert.AnError),
|
||||||
|
expectOK: assert.True,
|
||||||
|
expectErr: func(t *testing.T, ce *core.Err) {
|
||||||
|
assert.Equal(t, cErr, ce)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrapped",
|
||||||
|
err: clues.Wrap(cErr, "wrapper"),
|
||||||
|
expectOK: assert.True,
|
||||||
|
expectErr: func(t *testing.T, ce *core.Err) {
|
||||||
|
assert.Equal(t, cErr, ce)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, test := range table {
|
||||||
|
suite.Run(test.name, func() {
|
||||||
|
t := suite.T()
|
||||||
|
err, ok := core.As(test.err)
|
||||||
|
|
||||||
|
test.expectOK(t, ok)
|
||||||
|
test.expectErr(t, err)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,35 +3,17 @@ package errs
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
)
|
)
|
||||||
|
|
||||||
// expose enums, rather than errors, for Is checks. The enum should
|
|
||||||
// map to a specific internal error that can be used for the actual
|
|
||||||
// errors.Is comparison.
|
|
||||||
type errEnum string
|
|
||||||
|
|
||||||
const (
|
|
||||||
ApplicationThrottled errEnum = "application-throttled"
|
|
||||||
BackupNotFound errEnum = "backup-not-found"
|
|
||||||
InsufficientAuthorization errEnum = "insufficient-authorization"
|
|
||||||
RepoAlreadyExists errEnum = "repository-already-exists"
|
|
||||||
ResourceNotAccessible errEnum = "resource-not-accesible"
|
|
||||||
ResourceOwnerNotFound errEnum = "resource-owner-not-found"
|
|
||||||
ServiceNotEnabled errEnum = "service-not-enabled"
|
|
||||||
)
|
|
||||||
|
|
||||||
// map of enums to errors. We might want to re-use an enum for multiple
|
// map of enums to errors. We might want to re-use an enum for multiple
|
||||||
// internal errors (ex: "ServiceNotEnabled" may exist in both graph and
|
// internal errors.
|
||||||
// non-graph producers).
|
var externalToInternal = map[*core.Err][]error{
|
||||||
var externalToInternal = map[errEnum][]error{
|
core.ErrBackupNotFound: {repository.ErrorBackupNotFound},
|
||||||
ApplicationThrottled: {graph.ErrApplicationThrottled},
|
core.ErrRepoAlreadyExists: {repository.ErrorRepoAlreadyExists},
|
||||||
BackupNotFound: {repository.ErrorBackupNotFound},
|
core.ErrResourceNotAccessible: {graph.ErrResourceLocked},
|
||||||
RepoAlreadyExists: {repository.ErrorRepoAlreadyExists},
|
|
||||||
ResourceNotAccessible: {graph.ErrResourceLocked},
|
|
||||||
ResourceOwnerNotFound: {graph.ErrResourceOwnerNotFound},
|
|
||||||
ServiceNotEnabled: {graph.ErrServiceNotEnabled},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ErrCheck func(error) bool
|
type ErrCheck func(error) bool
|
||||||
@ -41,23 +23,27 @@ type ErrCheck func(error) bool
|
|||||||
// many places of error handling, we primarily rely on error comparison
|
// many places of error handling, we primarily rely on error comparison
|
||||||
// checks. This allows us to apply those comparison checks instead of relying
|
// checks. This allows us to apply those comparison checks instead of relying
|
||||||
// only on sentinels.
|
// only on sentinels.
|
||||||
var externalToInternalCheck = map[errEnum][]ErrCheck{
|
var externalToInternalCheck = map[*core.Err][]ErrCheck{
|
||||||
ApplicationThrottled: {graph.IsErrApplicationThrottled},
|
core.ErrApplicationThrottled: {graph.IsErrApplicationThrottled},
|
||||||
ResourceNotAccessible: {graph.IsErrResourceLocked},
|
core.ErrResourceNotAccessible: {graph.IsErrResourceLocked},
|
||||||
ResourceOwnerNotFound: {graph.IsErrItemNotFound},
|
core.ErrResourceOwnerNotFound: {graph.IsErrItemNotFound},
|
||||||
InsufficientAuthorization: {graph.IsErrInsufficientAuthorization},
|
core.ErrInsufficientAuthorization: {graph.IsErrInsufficientAuthorization},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Internal returns the internal errors and error checking functions which
|
// Internal returns the internal errors and error checking functions which
|
||||||
// match to the public error enum.
|
// match to the public error enum.
|
||||||
func Internal(enum errEnum) ([]error, []ErrCheck) {
|
func Internal(ce *core.Err) ([]error, []ErrCheck) {
|
||||||
return externalToInternal[enum], externalToInternalCheck[enum]
|
return externalToInternal[ce], externalToInternalCheck[ce]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is checks if the provided error contains an internal error that matches
|
// Is checks if the provided error contains an internal error that matches
|
||||||
// the public error category.
|
// the public error category.
|
||||||
func Is(err error, enum errEnum) bool {
|
func Is(err error, ce *core.Err) bool {
|
||||||
internalErrs, ok := externalToInternal[enum]
|
if errors.Is(err, ce) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
internalErrs, ok := externalToInternal[ce]
|
||||||
if ok {
|
if ok {
|
||||||
for _, target := range internalErrs {
|
for _, target := range internalErrs {
|
||||||
if errors.Is(err, target) {
|
if errors.Is(err, target) {
|
||||||
@ -66,7 +52,7 @@ func Is(err error, enum errEnum) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internalChecks, ok := externalToInternalCheck[enum]
|
internalChecks, ok := externalToInternalCheck[ce]
|
||||||
if ok {
|
if ok {
|
||||||
for _, check := range internalChecks {
|
for _, check := range internalChecks {
|
||||||
if check(err) {
|
if check(err) {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"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/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/repository"
|
"github.com/alcionai/corso/src/pkg/repository"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
|
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
|
||||||
@ -23,36 +24,24 @@ func TestErrUnitSuite(t *testing.T) {
|
|||||||
|
|
||||||
func (suite *ErrUnitSuite) TestInternal_errs() {
|
func (suite *ErrUnitSuite) TestInternal_errs() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
get errEnum
|
get *core.Err
|
||||||
expect []error
|
expect []error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
get: ApplicationThrottled,
|
get: core.ErrRepoAlreadyExists,
|
||||||
expect: []error{graph.ErrApplicationThrottled},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
get: RepoAlreadyExists,
|
|
||||||
expect: []error{repository.ErrorRepoAlreadyExists},
|
expect: []error{repository.ErrorRepoAlreadyExists},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: BackupNotFound,
|
get: core.ErrBackupNotFound,
|
||||||
expect: []error{repository.ErrorBackupNotFound},
|
expect: []error{repository.ErrorBackupNotFound},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: ServiceNotEnabled,
|
get: core.ErrResourceNotAccessible,
|
||||||
expect: []error{graph.ErrServiceNotEnabled},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
get: ResourceOwnerNotFound,
|
|
||||||
expect: []error{graph.ErrResourceOwnerNotFound},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
get: ResourceNotAccessible,
|
|
||||||
expect: []error{graph.ErrResourceLocked},
|
expect: []error{graph.ErrResourceLocked},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(string(test.get), func() {
|
suite.Run(test.get.Error(), func() {
|
||||||
// can't compare func signatures
|
// can't compare func signatures
|
||||||
errs, _ := Internal(test.get)
|
errs, _ := Internal(test.get)
|
||||||
assert.ElementsMatch(suite.T(), test.expect, errs)
|
assert.ElementsMatch(suite.T(), test.expect, errs)
|
||||||
@ -62,57 +51,56 @@ func (suite *ErrUnitSuite) TestInternal_errs() {
|
|||||||
|
|
||||||
func (suite *ErrUnitSuite) TestInternal_checks() {
|
func (suite *ErrUnitSuite) TestInternal_checks() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
get errEnum
|
get *core.Err
|
||||||
err error
|
err error
|
||||||
expectHasChecks assert.ValueAssertionFunc
|
expectHasChecks assert.ValueAssertionFunc
|
||||||
expect assert.BoolAssertionFunc
|
expect assert.BoolAssertionFunc
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
get: ApplicationThrottled,
|
get: core.ErrApplicationThrottled,
|
||||||
err: graph.ErrApplicationThrottled,
|
err: graphTD.ODataErr(string(graph.ApplicationThrottled)),
|
||||||
expectHasChecks: assert.NotEmpty,
|
expectHasChecks: assert.NotEmpty,
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: RepoAlreadyExists,
|
get: core.ErrRepoAlreadyExists,
|
||||||
err: graph.ErrApplicationThrottled,
|
err: graphTD.ODataErr(string(graph.ApplicationThrottled)),
|
||||||
expectHasChecks: assert.Empty,
|
expectHasChecks: assert.Empty,
|
||||||
expect: assert.False,
|
expect: assert.False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: BackupNotFound,
|
get: core.ErrBackupNotFound,
|
||||||
err: repository.ErrorBackupNotFound,
|
err: repository.ErrorBackupNotFound,
|
||||||
expectHasChecks: assert.Empty,
|
expectHasChecks: assert.Empty,
|
||||||
expect: assert.False,
|
expect: assert.False,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: ServiceNotEnabled,
|
get: core.ErrResourceOwnerNotFound,
|
||||||
err: graph.ErrServiceNotEnabled,
|
err: graphTD.ODataErr(string(graph.ItemNotFound)),
|
||||||
expectHasChecks: assert.Empty,
|
|
||||||
expect: assert.False,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
get: ResourceOwnerNotFound,
|
|
||||||
// won't match, checks itemNotFound, which isn't an error enum
|
|
||||||
err: graph.ErrResourceOwnerNotFound,
|
|
||||||
expectHasChecks: assert.NotEmpty,
|
expectHasChecks: assert.NotEmpty,
|
||||||
expect: assert.False,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: ResourceNotAccessible,
|
get: core.ErrResourceOwnerNotFound,
|
||||||
|
err: graphTD.ODataErr(string(graph.ErrorItemNotFound)),
|
||||||
|
expectHasChecks: assert.NotEmpty,
|
||||||
|
expect: assert.True,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
get: core.ErrResourceNotAccessible,
|
||||||
err: graph.ErrResourceLocked,
|
err: graph.ErrResourceLocked,
|
||||||
expectHasChecks: assert.NotEmpty,
|
expectHasChecks: assert.NotEmpty,
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
get: InsufficientAuthorization,
|
get: core.ErrInsufficientAuthorization,
|
||||||
err: graphTD.ODataErr(string(graph.AuthorizationRequestDenied)),
|
err: graphTD.ODataErr(string(graph.AuthorizationRequestDenied)),
|
||||||
expectHasChecks: assert.NotEmpty,
|
expectHasChecks: assert.NotEmpty,
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(string(test.get), func() {
|
suite.Run(test.get.Error(), func() {
|
||||||
t := suite.T()
|
t := suite.T()
|
||||||
|
|
||||||
_, checks := Internal(test.get)
|
_, checks := Internal(test.get)
|
||||||
@ -135,40 +123,28 @@ func (suite *ErrUnitSuite) TestInternal_checks() {
|
|||||||
|
|
||||||
func (suite *ErrUnitSuite) TestIs() {
|
func (suite *ErrUnitSuite) TestIs() {
|
||||||
table := []struct {
|
table := []struct {
|
||||||
target errEnum
|
target *core.Err
|
||||||
err error
|
err error
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
target: ApplicationThrottled,
|
target: core.ErrRepoAlreadyExists,
|
||||||
err: graph.ErrApplicationThrottled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: RepoAlreadyExists,
|
|
||||||
err: repository.ErrorRepoAlreadyExists,
|
err: repository.ErrorRepoAlreadyExists,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: BackupNotFound,
|
target: core.ErrBackupNotFound,
|
||||||
err: repository.ErrorBackupNotFound,
|
err: repository.ErrorBackupNotFound,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: ServiceNotEnabled,
|
target: core.ErrResourceNotAccessible,
|
||||||
err: graph.ErrServiceNotEnabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: ResourceOwnerNotFound,
|
|
||||||
err: graph.ErrResourceOwnerNotFound,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
target: ResourceNotAccessible,
|
|
||||||
err: graph.ErrResourceLocked,
|
err: graph.ErrResourceLocked,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
target: InsufficientAuthorization,
|
target: core.ErrInsufficientAuthorization,
|
||||||
err: graphTD.ODataErr(string(graph.AuthorizationRequestDenied)),
|
err: graphTD.ODataErr(string(graph.AuthorizationRequestDenied)),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, test := range table {
|
for _, test := range table {
|
||||||
suite.Run(string(test.target), func() {
|
suite.Run(test.target.Error(), func() {
|
||||||
var (
|
var (
|
||||||
w = clues.Wrap(test.err, "wrap")
|
w = clues.Wrap(test.err, "wrap")
|
||||||
s = clues.Stack(test.err)
|
s = clues.Stack(test.err)
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import (
|
|||||||
type errorCode string
|
type errorCode string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
applicationThrottled errorCode = "ApplicationThrottled"
|
ApplicationThrottled errorCode = "ApplicationThrottled"
|
||||||
// this authN error is a catch-all used by graph in a variety of cases:
|
// this authN error is a catch-all used by graph in a variety of cases:
|
||||||
// users without licenses, bad jwts, missing account permissions, etc.
|
// users without licenses, bad jwts, missing account permissions, etc.
|
||||||
AuthenticationError errorCode = "AuthenticationError"
|
AuthenticationError errorCode = "AuthenticationError"
|
||||||
@ -43,7 +43,7 @@ const (
|
|||||||
cannotOpenFileAttachment errorCode = "ErrorCannotOpenFileAttachment"
|
cannotOpenFileAttachment errorCode = "ErrorCannotOpenFileAttachment"
|
||||||
emailFolderNotFound errorCode = "ErrorSyncFolderNotFound"
|
emailFolderNotFound errorCode = "ErrorSyncFolderNotFound"
|
||||||
ErrorAccessDenied errorCode = "ErrorAccessDenied"
|
ErrorAccessDenied errorCode = "ErrorAccessDenied"
|
||||||
errorItemNotFound errorCode = "ErrorItemNotFound"
|
ErrorItemNotFound errorCode = "ErrorItemNotFound"
|
||||||
// This error occurs when an email is enumerated but retrieving it fails
|
// This error occurs when an email is enumerated but retrieving it fails
|
||||||
// - we believe - due to it pre-dating mailbox creation. Possible explanations
|
// - we believe - due to it pre-dating mailbox creation. Possible explanations
|
||||||
// are mailbox creation racing with email receipt or a similar issue triggered
|
// are mailbox creation racing with email receipt or a similar issue triggered
|
||||||
@ -57,7 +57,7 @@ const (
|
|||||||
// that doesn't exist.
|
// that doesn't exist.
|
||||||
invalidUser errorCode = "ErrorInvalidUser"
|
invalidUser errorCode = "ErrorInvalidUser"
|
||||||
invalidAuthenticationToken errorCode = "InvalidAuthenticationToken"
|
invalidAuthenticationToken errorCode = "InvalidAuthenticationToken"
|
||||||
itemNotFound errorCode = "itemNotFound"
|
ItemNotFound 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
|
||||||
@ -104,11 +104,10 @@ const (
|
|||||||
LabelsSkippable = "skippable_errors"
|
LabelsSkippable = "skippable_errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// These errors are graph specific. That means they don't have a clear parallel in
|
||||||
|
// pkg/errs/core. If these errors need to trickle outward to non-m365 layers, we
|
||||||
|
// need to find a sufficiently coarse errs/core sentinel to use as transformation.
|
||||||
var (
|
var (
|
||||||
// ErrApplicationThrottled occurs if throttling retries are exhausted and completely
|
|
||||||
// fails out.
|
|
||||||
ErrApplicationThrottled = clues.New("application throttled")
|
|
||||||
|
|
||||||
// The folder or item was deleted between the time we identified
|
// The folder or item was deleted between the time we identified
|
||||||
// it and when we tried to fetch data for it.
|
// it and when we tried to fetch data for it.
|
||||||
ErrDeletedInFlight = clues.New("deleted in flight")
|
ErrDeletedInFlight = clues.New("deleted in flight")
|
||||||
@ -131,18 +130,11 @@ var (
|
|||||||
// This makes the resource inaccessible for any Corso operations.
|
// This makes the resource inaccessible for any Corso operations.
|
||||||
ErrResourceLocked = clues.New("resource has been locked and must be unlocked by an administrator")
|
ErrResourceLocked = clues.New("resource has been locked and must be unlocked by an administrator")
|
||||||
|
|
||||||
// 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")
|
|
||||||
|
|
||||||
ErrResourceOwnerNotFound = clues.New("resource owner not found in tenant")
|
|
||||||
|
|
||||||
ErrTokenExpired = clues.New("jwt token expired")
|
ErrTokenExpired = clues.New("jwt token expired")
|
||||||
)
|
)
|
||||||
|
|
||||||
func IsErrApplicationThrottled(err error) bool {
|
func IsErrApplicationThrottled(err error) bool {
|
||||||
return errors.Is(err, ErrApplicationThrottled) ||
|
return parseODataErr(err).hasErrorCode(err, ApplicationThrottled)
|
||||||
parseODataErr(err).hasErrorCode(err, applicationThrottled)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsErrAuthenticationError(err error) bool {
|
func IsErrAuthenticationError(err error) bool {
|
||||||
@ -160,8 +152,8 @@ func IsErrDeletedInFlight(err error) bool {
|
|||||||
|
|
||||||
if parseODataErr(err).hasErrorCode(
|
if parseODataErr(err).hasErrorCode(
|
||||||
err,
|
err,
|
||||||
errorItemNotFound,
|
ErrorItemNotFound,
|
||||||
itemNotFound,
|
ItemNotFound,
|
||||||
syncFolderNotFound) {
|
syncFolderNotFound) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
@ -170,7 +162,7 @@ func IsErrDeletedInFlight(err error) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func IsErrItemNotFound(err error) bool {
|
func IsErrItemNotFound(err error) bool {
|
||||||
return parseODataErr(err).hasErrorCode(err, itemNotFound, errorItemNotFound)
|
return parseODataErr(err).hasErrorCode(err, ItemNotFound, ErrorItemNotFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsErrInvalidDelta(err error) bool {
|
func IsErrInvalidDelta(err error) bool {
|
||||||
@ -188,7 +180,7 @@ func IsErrQuotaExceeded(err error) bool {
|
|||||||
func IsErrExchangeMailFolderNotFound(err error) bool {
|
func IsErrExchangeMailFolderNotFound(err error) bool {
|
||||||
// Not sure if we can actually see a resourceNotFound error here. I've only
|
// Not sure if we can actually see a resourceNotFound error here. I've only
|
||||||
// seen the latter two.
|
// seen the latter two.
|
||||||
return parseODataErr(err).hasErrorCode(err, ResourceNotFound, errorItemNotFound, MailboxNotEnabledForRESTAPI)
|
return parseODataErr(err).hasErrorCode(err, ResourceNotFound, ErrorItemNotFound, MailboxNotEnabledForRESTAPI)
|
||||||
}
|
}
|
||||||
|
|
||||||
func IsErrUserNotFound(err error) bool {
|
func IsErrUserNotFound(err error) bool {
|
||||||
|
|||||||
@ -79,7 +79,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrApplicationThrottled() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "applicationThrottled oDataErr",
|
name: "applicationThrottled oDataErr",
|
||||||
err: graphTD.ODataErr(string(applicationThrottled)),
|
err: graphTD.ODataErr(string(ApplicationThrottled)),
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -186,7 +186,7 @@ func (suite *GraphErrorsUnitSuite) TestIsErrDeletedInFlight() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "not-found oDataErr",
|
name: "not-found oDataErr",
|
||||||
err: graphTD.ODataErr(string(errorItemNotFound)),
|
err: graphTD.ODataErr(string(ErrorItemNotFound)),
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -858,12 +858,12 @@ func (suite *GraphErrorsUnitSuite) TestIsErrItemNotFound() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "item not found oDataErr",
|
name: "item not found oDataErr",
|
||||||
err: graphTD.ODataErr(string(itemNotFound)),
|
err: graphTD.ODataErr(string(ItemNotFound)),
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "error item not found oDataErr",
|
name: "error item not found oDataErr",
|
||||||
err: graphTD.ODataErr(string(errorItemNotFound)),
|
err: graphTD.ODataErr(string(ErrorItemNotFound)),
|
||||||
expect: assert.True,
|
expect: assert.True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/events"
|
"github.com/alcionai/corso/src/internal/events"
|
||||||
"github.com/alcionai/corso/src/internal/version"
|
"github.com/alcionai/corso/src/internal/version"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -120,7 +121,7 @@ func (hw httpWrapper) Request(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if IsErrApplicationThrottled(err) {
|
if IsErrApplicationThrottled(err) {
|
||||||
return nil, Stack(ctx, clues.Stack(ErrApplicationThrottled, err))
|
return nil, Stack(ctx, clues.Stack(core.ErrApplicationThrottled, err))
|
||||||
}
|
}
|
||||||
|
|
||||||
var http2StreamErr http2.StreamError
|
var http2StreamErr http2.StreamError
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/events"
|
"github.com/alcionai/corso/src/internal/events"
|
||||||
"github.com/alcionai/corso/src/pkg/count"
|
"github.com/alcionai/corso/src/pkg/count"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/filters"
|
"github.com/alcionai/corso/src/pkg/filters"
|
||||||
"github.com/alcionai/corso/src/pkg/logger"
|
"github.com/alcionai/corso/src/pkg/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/path"
|
"github.com/alcionai/corso/src/pkg/path"
|
||||||
@ -395,7 +396,7 @@ func (aw *adapterWrap) Send(
|
|||||||
// those retries are well handled in middleware already. We want to ensure
|
// those retries are well handled in middleware already. We want to ensure
|
||||||
// that the error gets wrapped with the appropriate sentinel here.
|
// that the error gets wrapped with the appropriate sentinel here.
|
||||||
if IsErrApplicationThrottled(err) {
|
if IsErrApplicationThrottled(err) {
|
||||||
return nil, clues.StackWC(ictx, ErrApplicationThrottled, err).WithTrace(1)
|
return nil, clues.StackWC(ictx, core.ErrApplicationThrottled, err).WithTrace(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
// exit most errors without retry
|
// exit most errors without retry
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"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/pkg/errs/core"
|
||||||
"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/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
@ -204,7 +205,7 @@ func getGroupFromResponse(ctx context.Context, resp models.GroupCollectionRespon
|
|||||||
vs := resp.GetValue()
|
vs := resp.GetValue()
|
||||||
|
|
||||||
if len(vs) == 0 {
|
if len(vs) == 0 {
|
||||||
return nil, clues.StackWC(ctx, graph.ErrResourceOwnerNotFound)
|
return nil, clues.StackWC(ctx, core.ErrResourceOwnerNotFound)
|
||||||
} else if len(vs) > 1 {
|
} else if len(vs) > 1 {
|
||||||
return nil, clues.StackWC(ctx, graph.ErrMultipleResultsMatchIdentifier)
|
return nil, clues.StackWC(ctx, graph.ErrMultipleResultsMatchIdentifier)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -14,6 +14,7 @@ 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/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
|
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
|
||||||
@ -201,7 +202,7 @@ func (suite *GroupsIntgSuite) TestGroups_GetByID() {
|
|||||||
name: "invalid 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.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
assert.ErrorIs(t, err, core.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -215,7 +216,7 @@ func (suite *GroupsIntgSuite) TestGroups_GetByID() {
|
|||||||
name: "invalid displayName",
|
name: "invalid displayName",
|
||||||
id: "jabberwocky",
|
id: "jabberwocky",
|
||||||
expectErr: func(t *testing.T, err error) {
|
expectErr: func(t *testing.T, err error) {
|
||||||
assert.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
assert.ErrorIs(t, err, core.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
"github.com/microsoftgraph/msgraph-sdk-go/sites"
|
"github.com/microsoftgraph/msgraph-sdk-go/sites"
|
||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
)
|
)
|
||||||
@ -153,7 +154,7 @@ func (c Sites) GetByID(
|
|||||||
// a 404 when getting sites by ID returns an itemNotFound
|
// a 404 when getting sites by ID returns an itemNotFound
|
||||||
// error code, instead of something more sensible.
|
// error code, instead of something more sensible.
|
||||||
if graph.IsErrItemNotFound(err) {
|
if graph.IsErrItemNotFound(err) {
|
||||||
err = clues.Stack(graph.ErrResourceOwnerNotFound, err)
|
err = clues.Stack(core.ErrResourceOwnerNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.IsErrResourceLocked(err) {
|
if graph.IsErrResourceLocked(err) {
|
||||||
@ -199,7 +200,7 @@ func (c Sites) GetByID(
|
|||||||
// a 404 when getting sites by ID returns an itemNotFound
|
// a 404 when getting sites by ID returns an itemNotFound
|
||||||
// error code, instead of something more sensible.
|
// error code, instead of something more sensible.
|
||||||
if graph.IsErrItemNotFound(err) {
|
if graph.IsErrItemNotFound(err) {
|
||||||
err = clues.Stack(graph.ErrResourceOwnerNotFound, err)
|
err = clues.Stack(core.ErrResourceOwnerNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.IsErrResourceLocked(err) {
|
if graph.IsErrResourceLocked(err) {
|
||||||
|
|||||||
@ -14,8 +14,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/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SitesUnitSuite struct {
|
type SitesUnitSuite struct {
|
||||||
@ -181,7 +181,7 @@ func (suite *SitesIntgSuite) TestSites_GetByID() {
|
|||||||
name: "random id",
|
name: "random id",
|
||||||
id: uuid.NewString() + "," + uuid.NewString(),
|
id: uuid.NewString() + "," + uuid.NewString(),
|
||||||
expectErr: func(t *testing.T, err error) bool {
|
expectErr: func(t *testing.T, err error) bool {
|
||||||
assert.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
assert.ErrorIs(t, err, core.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@ -213,7 +213,7 @@ func (suite *SitesIntgSuite) TestSites_GetByID() {
|
|||||||
name: "well formed url, no sites match",
|
name: "well formed url, no sites match",
|
||||||
id: modifiedSiteURL,
|
id: modifiedSiteURL,
|
||||||
expectErr: func(t *testing.T, err error) bool {
|
expectErr: func(t *testing.T, err error) bool {
|
||||||
assert.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
assert.ErrorIs(t, err, core.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
return true
|
return true
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -13,6 +13,7 @@ import (
|
|||||||
|
|
||||||
"github.com/alcionai/corso/src/internal/common/idname"
|
"github.com/alcionai/corso/src/internal/common/idname"
|
||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"github.com/alcionai/corso/src/internal/common/ptr"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
)
|
)
|
||||||
@ -198,7 +199,7 @@ func EvaluateMailboxError(err error) error {
|
|||||||
|
|
||||||
// must occur before MailFolderNotFound, due to overlapping cases.
|
// must occur before MailFolderNotFound, due to overlapping cases.
|
||||||
if graph.IsErrUserNotFound(err) {
|
if graph.IsErrUserNotFound(err) {
|
||||||
return clues.Stack(graph.ErrResourceOwnerNotFound, err)
|
return clues.Stack(core.ErrResourceOwnerNotFound, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if graph.IsErrResourceLocked(err) {
|
if graph.IsErrResourceLocked(err) {
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import (
|
|||||||
"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/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
|
graphTD "github.com/alcionai/corso/src/pkg/services/m365/api/graph/testdata"
|
||||||
)
|
)
|
||||||
@ -82,7 +83,7 @@ func (suite *UsersUnitSuite) TestEvaluateMailboxError() {
|
|||||||
name: "mail inbox err - user not found",
|
name: "mail inbox err - user not found",
|
||||||
err: graphTD.ODataErr(string(graph.RequestResourceNotFound)),
|
err: graphTD.ODataErr(string(graph.RequestResourceNotFound)),
|
||||||
expect: func(t *testing.T, err error) {
|
expect: func(t *testing.T, err error) {
|
||||||
assert.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
assert.ErrorIs(t, err, core.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@ -12,8 +12,8 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/tester"
|
"github.com/alcionai/corso/src/internal/tester"
|
||||||
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
"github.com/alcionai/corso/src/pkg/errs"
|
"github.com/alcionai/corso/src/pkg/errs"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type GroupsIntgSuite struct {
|
type GroupsIntgSuite struct {
|
||||||
@ -108,8 +108,8 @@ func (suite *GroupsIntgSuite) TestGroupByID_notFound() {
|
|||||||
|
|
||||||
group, err := suite.cli.GroupByID(ctx, uuid.NewString())
|
group, err := suite.cli.GroupByID(ctx, uuid.NewString())
|
||||||
require.Nil(t, group)
|
require.Nil(t, group)
|
||||||
require.ErrorIs(t, err, graph.ErrResourceOwnerNotFound, clues.ToCore(err))
|
require.ErrorIs(t, err, core.ErrResourceOwnerNotFound, clues.ToCore(err))
|
||||||
require.True(t, errs.Is(err, errs.ResourceOwnerNotFound))
|
require.True(t, errs.Is(err, core.ErrResourceOwnerNotFound))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *GroupsIntgSuite) TestGroups() {
|
func (suite *GroupsIntgSuite) TestGroups() {
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/alcionai/corso/src/internal/common/ptr"
|
"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/pkg/errs/core"
|
||||||
"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/logger"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
@ -87,7 +88,7 @@ func getAllSites(
|
|||||||
sites, err := ga.GetAll(ctx, fault.New(true))
|
sites, err := ga.GetAll(ctx, fault.New(true))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
|
if clues.HasLabel(err, graph.LabelsNoSharePointLicense) {
|
||||||
return nil, clues.Stack(graph.ErrServiceNotEnabled, err)
|
return nil, clues.Stack(core.ErrServiceNotEnabled, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, clues.Wrap(err, "retrieving sites")
|
return nil, clues.Wrap(err, "retrieving sites")
|
||||||
|
|||||||
@ -14,6 +14,7 @@ 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/internal/tester/tconfig"
|
"github.com/alcionai/corso/src/internal/tester/tconfig"
|
||||||
|
"github.com/alcionai/corso/src/pkg/errs/core"
|
||||||
"github.com/alcionai/corso/src/pkg/fault"
|
"github.com/alcionai/corso/src/pkg/fault"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
"github.com/alcionai/corso/src/pkg/services/m365/api"
|
||||||
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
"github.com/alcionai/corso/src/pkg/services/m365/api/graph"
|
||||||
@ -145,7 +146,7 @@ func (suite *siteUnitSuite) TestGetAllSites() {
|
|||||||
return mockGASites{nil, graph.Stack(ctx, odErr)}
|
return mockGASites{nil, graph.Stack(ctx, odErr)}
|
||||||
},
|
},
|
||||||
expectErr: func(t *testing.T, err error) {
|
expectErr: func(t *testing.T, err error) {
|
||||||
assert.ErrorIs(t, err, graph.ErrServiceNotEnabled, clues.ToCore(err))
|
assert.ErrorIs(t, err, core.ErrServiceNotEnabled, clues.ToCore(err))
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user