mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
`pmg cloud sync` without usable credentials surfaced misleading errors:
missing credentials fell into the generic network fallback ("check your
network connectivity") and server-rejected credentials (gateway 403)
showed a bare "Permission denied" with no remediation.
Map dry's cloud.ErrMissingCredentials to a new CloudCredentialsNotFound
code pointing at 'pmg cloud login' and the SAFEDEP_API_KEY /
SAFEDEP_TENANT_ID environment variables, and rebrand authentication and
authorization failures with the same credential guidance. Entitlement,
quota and server errors still pass through unchanged.
128 lines
4.3 KiB
Go
128 lines
4.3 KiB
Go
package cloud
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/safedep/dry/cloud"
|
|
"github.com/safedep/dry/usefulerror"
|
|
"github.com/safedep/pmg/errcodes"
|
|
"github.com/safedep/pmg/internal/audit"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/genproto/googleapis/rpc/errdetails"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
)
|
|
|
|
func entitlementStatusErr(t *testing.T, anyNesting int) error {
|
|
t.Helper()
|
|
|
|
detail, err := anypb.New(&errdetails.ErrorInfo{
|
|
Reason: "entitlement_not_available",
|
|
Domain: "safedep.io",
|
|
Metadata: map[string]string{
|
|
"feature": "FEATURE_ENDPOINT_SYNC",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
for range anyNesting {
|
|
detail, err = anypb.New(detail)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
st := status.New(codes.PermissionDenied, "service execution failed: entitlement verification failed")
|
|
stProto := st.Proto()
|
|
stProto.Details = append(stProto.Details, detail)
|
|
|
|
return status.FromProto(stProto).Err()
|
|
}
|
|
|
|
func TestSyncFailureError(t *testing.T) {
|
|
wrap := func(err error) error {
|
|
return fmt.Errorf("endpointsync: sync failed: %w", err)
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
err error
|
|
expectedCode string
|
|
expectedHuman string
|
|
expectedHelp string
|
|
}{
|
|
{
|
|
name: "sync already in progress",
|
|
err: fmt.Errorf("outer: %w", audit.ErrSyncInProgress),
|
|
expectedCode: errcodes.Lifecycle,
|
|
expectedHuman: "Another cloud sync is already in progress",
|
|
},
|
|
{
|
|
name: "missing credentials maps to setup guidance",
|
|
err: fmt.Errorf("init cloud sync client: failed to resolve cloud credentials: %w", cloud.ErrMissingCredentials),
|
|
expectedCode: errcodes.CloudCredentialsNotFound,
|
|
expectedHuman: "SafeDep Cloud credentials are not configured",
|
|
expectedHelp: "Run 'pmg cloud login' to store credentials, or set the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables",
|
|
},
|
|
{
|
|
name: "authentication failure gets credential guidance",
|
|
err: wrap(status.Error(codes.Unauthenticated, "invalid API key")),
|
|
expectedCode: usefulerror.ErrAuthenticationFailed,
|
|
expectedHuman: "SafeDep Cloud rejected your credentials",
|
|
expectedHelp: "Run 'pmg cloud login' to update credentials, or check the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables",
|
|
},
|
|
{
|
|
name: "entitlement failure passes through",
|
|
err: wrap(entitlementStatusErr(t, 0)),
|
|
expectedCode: usefulerror.ErrMissingEntitlements,
|
|
expectedHuman: "Permission denied",
|
|
expectedHelp: "Access to this feature requires a SafeDep subscription. See https://safedep.io/pricing",
|
|
},
|
|
{
|
|
name: "entitlement failure with re-wrapped details passes through",
|
|
// Old control-tower serror.Add re-packed details into nested Any
|
|
// layers; classification must survive that wire format too.
|
|
err: wrap(entitlementStatusErr(t, 2)),
|
|
expectedCode: usefulerror.ErrMissingEntitlements,
|
|
expectedHuman: "Permission denied",
|
|
},
|
|
{
|
|
name: "permission denied without entitlement detail gets credential guidance",
|
|
err: wrap(status.Error(codes.PermissionDenied, "no access")),
|
|
expectedCode: usefulerror.ErrAuthorizationFailed,
|
|
expectedHuman: "SafeDep Cloud rejected your credentials",
|
|
expectedHelp: "Run 'pmg cloud login' to update credentials, or check the SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables",
|
|
},
|
|
{
|
|
name: "server internal error passes through",
|
|
err: wrap(status.Error(codes.Internal, "server closed the stream without sending trailers")),
|
|
expectedCode: usefulerror.ErrInternalServerError,
|
|
expectedHuman: "Internal server error",
|
|
},
|
|
{
|
|
name: "unclassifiable error falls back to network",
|
|
err: errors.New("something odd happened"),
|
|
expectedCode: errcodes.Network,
|
|
expectedHuman: "Failed to sync events to SafeDep Cloud",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := syncFailureError(tt.err)
|
|
require.Error(t, err)
|
|
|
|
usefulErr, ok := usefulerror.AsUsefulError(err)
|
|
require.True(t, ok)
|
|
|
|
assert.Equal(t, tt.expectedCode, usefulErr.Code())
|
|
assert.Equal(t, tt.expectedHuman, usefulErr.HumanError())
|
|
if tt.expectedHelp != "" {
|
|
assert.Equal(t, tt.expectedHelp, usefulErr.Help())
|
|
}
|
|
})
|
|
}
|
|
}
|