mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 13:52:51 +00:00
# Which Problems Are Solved As part of https://github.com/zitadel/zitadel/issues/11917 we want to introduce error slugs so (API) clients can rely on stable, machine-readable errors and act accordingly. # How the Problems Are Solved - Added a `NewSlug` helper function in the domain package. - Added `ErrorDetails` to the `ZitadelError` - Added an `zitadel.error.v2.ErrorDetail` proto message - Updated the connectRPC error interceptor to map new slug based errors to the new `ErrorDetail` - Defined some common slugs and error functions like internal errors - Defined (session) specific slugs used in the `DeleteSession` and `CheckUser` functions and replaced old implementations - Updated integration tests to check specific errors if the relation database feature is enabled - Updated doc and guideline to reflect the latest changes and decisions - Updated DeleteSession endpoint API to list possible slugs # Additional Changes None # Additional Context - closes #11957 --------- Co-authored-by: Wim Van Laer <[email protected]>
269 lines
6.8 KiB
Go
269 lines
6.8 KiB
Go
package gerrors
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"log/slog"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/grpc/codes"
|
|
"google.golang.org/grpc/status"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/protoadapt"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
|
|
commandErrors "github.com/zitadel/zitadel/internal/command/errors"
|
|
"github.com/zitadel/zitadel/internal/zerrors"
|
|
errorpb "github.com/zitadel/zitadel/pkg/grpc/error/v2"
|
|
"github.com/zitadel/zitadel/pkg/grpc/message"
|
|
)
|
|
|
|
func TestCaosToGRPCError(t *testing.T) {
|
|
type args struct {
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantErr bool
|
|
}{
|
|
{
|
|
"no error",
|
|
args{},
|
|
false,
|
|
},
|
|
{
|
|
"unknown error",
|
|
args{errors.New("unknown")},
|
|
true,
|
|
},
|
|
{
|
|
"caos error",
|
|
args{zerrors.ThrowInternal(nil, "", "message")},
|
|
true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
if err := ZITADELToGRPCError(t.Context(), tt.args.err); (err != nil) != tt.wantErr {
|
|
t.Errorf("ZITADELToGRPCError() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestZITADELToGRPCError_PreservesGRPCStatus(t *testing.T) {
|
|
err := ZITADELToGRPCError(t.Context(), status.Error(codes.Unauthenticated, "auth header missing"))
|
|
grpcStatus, ok := status.FromError(err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, codes.Unauthenticated, grpcStatus.Code())
|
|
assert.Equal(t, "auth header missing", grpcStatus.Message())
|
|
}
|
|
|
|
func Test_getErrorInfo(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
id string
|
|
key string
|
|
err error
|
|
result protoadapt.MessageV1
|
|
}{
|
|
{
|
|
name: "parent error nil, return message.ErrorDetail{}",
|
|
id: "id",
|
|
key: "key",
|
|
result: &message.ErrorDetail{Id: "id", Message: "key"},
|
|
},
|
|
{
|
|
name: "parent error nil not commandErrors.WrongPasswordError{}, return message.ErrorDetail{}",
|
|
id: "id",
|
|
key: "key",
|
|
err: fmt.Errorf("normal error not commandErrors.WrongPasswordError{}"),
|
|
result: &message.ErrorDetail{Id: "id", Message: "key"},
|
|
},
|
|
{
|
|
name: "parent error not nil type commandErrors.WrongPasswordError{}, return message.CredentialsCheckError{}",
|
|
id: "id",
|
|
key: "key",
|
|
err: &commandErrors.WrongPasswordError{FailedAttempts: 22},
|
|
result: &message.CredentialsCheckError{Id: "id", Message: "key", FailedAttempts: 22},
|
|
},
|
|
{
|
|
name: "parent error not nil wrapped commandErrors.WrongPasswordError{}, return message.CredentialsCheckError{}",
|
|
id: "id",
|
|
key: "key",
|
|
err: func() error {
|
|
err := fmt.Errorf("normal error")
|
|
wpa := &commandErrors.WrongPasswordError{FailedAttempts: 26}
|
|
return fmt.Errorf("%w: %w", err, wpa)
|
|
}(),
|
|
result: &message.CredentialsCheckError{Id: "id", Message: "key", FailedAttempts: 26},
|
|
},
|
|
{
|
|
name: "parent error not nil wrapped zerrors.ZitadelError{} with id, message errorpb.ErrorDetail{}",
|
|
id: "id",
|
|
key: "key",
|
|
err: func() error {
|
|
err := fmt.Errorf("normal error")
|
|
zerr := &zerrors.ZitadelError{ID: "id", Message: "key"}
|
|
return fmt.Errorf("%w: %w", err, zerr)
|
|
}(),
|
|
result: &message.ErrorDetail{Id: "id", Message: "key"},
|
|
},
|
|
{
|
|
name: "parent error not nil wrapped zerrors.ZitadelError{} with slug, return errorpb.ErrorDetail{}",
|
|
id: "slug.reason",
|
|
key: "message",
|
|
err: func() error {
|
|
err := fmt.Errorf("normal error")
|
|
zerr := &zerrors.ZitadelError{ID: "slug.reason", Message: "message", Details: zerrors.ErrorDetailsMap{"some": "details"}}
|
|
return fmt.Errorf("%w: %w", err, zerr)
|
|
}(),
|
|
result: &errorpb.ErrorDetail{Slug: "slug.reason", Message: "message", Details: &structpb.Struct{Fields: map[string]*structpb.Value{"some": {Kind: &structpb.Value_StringValue{StringValue: "details"}}}}},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
errorInfo := getErrorInfo(t.Context(), tt.id, tt.key, tt.err)
|
|
if !proto.Equal(tt.result.(proto.Message), errorInfo.(proto.Message)) {
|
|
t.Errorf("getErrorInfo() = %v, want %v", errorInfo, tt.result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_extractError(t *testing.T) {
|
|
type args struct {
|
|
err error
|
|
}
|
|
tests := []struct {
|
|
name string
|
|
args args
|
|
wantC codes.Code
|
|
wantMsg string
|
|
wantID string
|
|
wantLvl slog.Level
|
|
}{
|
|
{
|
|
"already exists",
|
|
args{zerrors.ThrowAlreadyExists(nil, "id", "already exists")},
|
|
codes.AlreadyExists,
|
|
"already exists",
|
|
"id",
|
|
slog.LevelWarn,
|
|
},
|
|
{
|
|
"deadline exceeded",
|
|
args{zerrors.ThrowDeadlineExceeded(nil, "id", "deadline exceeded")},
|
|
codes.DeadlineExceeded,
|
|
"deadline exceeded",
|
|
"id",
|
|
slog.LevelError,
|
|
},
|
|
{
|
|
"internal error",
|
|
args{zerrors.ThrowInternal(nil, "id", "internal error")},
|
|
codes.Internal,
|
|
"internal error",
|
|
"id",
|
|
slog.LevelError,
|
|
},
|
|
{
|
|
"invalid argument",
|
|
args{zerrors.ThrowInvalidArgument(nil, "id", "invalid argument")},
|
|
codes.InvalidArgument,
|
|
"invalid argument",
|
|
"id",
|
|
slog.LevelWarn,
|
|
},
|
|
{
|
|
"not found",
|
|
args{zerrors.ThrowNotFound(nil, "id", "not found")},
|
|
codes.NotFound,
|
|
"not found",
|
|
"id",
|
|
slog.LevelWarn,
|
|
},
|
|
{
|
|
"permission denied",
|
|
args{zerrors.ThrowPermissionDenied(nil, "id", "permission denied")},
|
|
codes.PermissionDenied,
|
|
"permission denied",
|
|
"id",
|
|
slog.LevelWarn,
|
|
},
|
|
{
|
|
"precondition failed",
|
|
args{zerrors.ThrowPreconditionFailed(nil, "id", "precondition failed")},
|
|
codes.FailedPrecondition,
|
|
"precondition failed",
|
|
"id",
|
|
slog.LevelWarn,
|
|
},
|
|
{
|
|
"unauthenticated",
|
|
args{zerrors.ThrowUnauthenticated(nil, "id", "unauthenticated")},
|
|
codes.Unauthenticated,
|
|
"unauthenticated",
|
|
"id",
|
|
slog.LevelWarn,
|
|
},
|
|
{
|
|
"unavailable",
|
|
args{zerrors.ThrowUnavailable(nil, "id", "unavailable")},
|
|
codes.Unavailable,
|
|
"unavailable",
|
|
"id",
|
|
slog.LevelError,
|
|
},
|
|
{
|
|
"unimplemented",
|
|
args{zerrors.ThrowUnimplemented(nil, "id", "unimplemented")},
|
|
codes.Unimplemented,
|
|
"unimplemented",
|
|
"id",
|
|
slog.LevelInfo,
|
|
},
|
|
{
|
|
"exhausted",
|
|
args{zerrors.ThrowResourceExhausted(nil, "id", "exhausted")},
|
|
codes.ResourceExhausted,
|
|
"exhausted",
|
|
"id",
|
|
slog.LevelError,
|
|
},
|
|
{
|
|
"unknown",
|
|
args{errors.New("unknown")},
|
|
codes.Unknown,
|
|
"unknown",
|
|
"",
|
|
slog.LevelError,
|
|
},
|
|
{
|
|
"grpc unauthenticated",
|
|
args{status.Error(codes.Unauthenticated, "auth header missing")},
|
|
codes.Unauthenticated,
|
|
"auth header missing",
|
|
"",
|
|
slog.LevelWarn,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
gotC, gotMsg, gotID := ExtractZITADELError(tt.args.err)
|
|
if gotC != tt.wantC {
|
|
t.Errorf("extract() gotC = %v, want %v", gotC, tt.wantC)
|
|
}
|
|
if gotMsg != tt.wantMsg {
|
|
t.Errorf("extract() gotMsg = %v, want %v", gotMsg, tt.wantMsg)
|
|
}
|
|
if gotID != tt.wantID {
|
|
t.Errorf("extract() gotID = %v, want %v", gotID, tt.wantID)
|
|
}
|
|
})
|
|
}
|
|
}
|