feat(domain): introduce error slugs (#12030)

# 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]>
This commit is contained in:
Livio Spring
2026-04-13 09:47:04 +02:00
committed by GitHub
co-authored by Wim Van Laer
parent 5c9361f6c7
commit a2bf528ca0
18 changed files with 524 additions and 97 deletions
+58
View File
@@ -12,6 +12,8 @@ import (
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc/gerrors"
errorpb "github.com/zitadel/zitadel/pkg/grpc/error/v2"
metadata "github.com/zitadel/zitadel/pkg/grpc/metadata/v2"
resources_object "github.com/zitadel/zitadel/pkg/grpc/resources/object/v3alpha"
)
@@ -191,3 +193,59 @@ func getMetadataMap(metadataEntries []*metadata.Metadata) map[string][]byte {
}
return metadataByKey
}
func RequireStatusError(t *testing.T, err, targetErr error) {
t.Helper()
if AssertStatusError(t, err, targetErr) {
return
}
t.FailNow()
}
func AssertStatusError(t *testing.T, err, targetErr error) bool {
t.Helper()
cErr, ok := status.FromError(err)
if !ok {
return assert.Fail(t, "expected status error")
}
tErr, ok := status.FromError(gerrors.ZITADELToGRPCError(t.Context(), targetErr))
if !ok {
return assert.Fail(t, "expected status error")
}
if !assert.Equal(t, tErr.Code(), cErr.Code(), "expected different status code") {
return false
}
if len(tErr.Details()) == 1 {
if len(cErr.Details()) != 1 {
return assert.Fail(t, "missing details")
}
targetDetails, ok := tErr.Details()[0].(*errorpb.ErrorDetail)
if !ok {
return assert.Failf(t, "expected error detail in target error", "got %#v", tErr.Details()[0])
}
details, ok := cErr.Details()[0].(*errorpb.ErrorDetail)
if !ok {
return assert.Failf(t, "expected error detail in actual error", "got %#v", cErr.Details()[0])
}
return AssertErrorDetail(t, details, targetDetails)
}
return true
}
func AssertErrorDetail(t *testing.T, detail, targetDetail *errorpb.ErrorDetail) bool {
t.Helper()
if !assert.Equal(t, targetDetail.GetSlug(), detail.GetSlug(), "expected different slug") {
return false
}
if targetDetail.GetMessage() != "" && !assert.Equal(t, targetDetail.GetMessage(), detail.GetMessage(), "expected different message") {
return false
}
if targetDetail.GetDetails() != nil && !assert.Equal(t, targetDetail.GetDetails().AsMap(), detail.GetDetails().AsMap(), "expected different details") {
return false
}
return true
}