feat: implement register Passkey user API v2 (#5873)

* command/crypto: DRY the code

- reuse the the algorithm switch to create a secret generator
- add a verifyCryptoCode function

* command: crypto code tests

* migrate webauthn package

* finish integration tests with webauthn mock client
This commit is contained in:
Tim Möhlmann
2023-05-24 13:22:00 +03:00
committed by GitHub
parent 6839a5c203
commit a301c40f9f
44 changed files with 2528 additions and 517 deletions

View File

@@ -0,0 +1,16 @@
package authz
import (
"context"
"github.com/zitadel/zitadel/internal/errors"
)
// UserIDInCTX checks if the userID
// equals the authenticated user in the context.
func UserIDInCTX(ctx context.Context, userID string) error {
if GetCtxData(ctx).UserID != userID {
return errors.ThrowUnauthenticated(nil, "AUTH-Bohd2", "Errors.User.UserIDWrong")
}
return nil
}

View File

@@ -0,0 +1,36 @@
package grpc
import (
"testing"
"google.golang.org/protobuf/reflect/protoreflect"
)
// AllFieldsSet recusively checks if all values in a message
// have a non-zero value.
func AllFieldsSet(t testing.TB, msg protoreflect.Message, ignoreTypes ...protoreflect.FullName) {
ignore := make(map[protoreflect.FullName]bool, len(ignoreTypes))
for _, name := range ignoreTypes {
ignore[name] = true
}
md := msg.Descriptor()
name := md.FullName()
if ignore[name] {
return
}
fields := md.Fields()
for i := 0; i < fields.Len(); i++ {
fd := fields.Get(i)
if !msg.Has(fd) {
t.Errorf("not all fields set in %q, missing %q", name, fd.Name())
continue
}
if fd.Kind() == protoreflect.MessageKind {
AllFieldsSet(t, msg.Get(fd).Message(), ignoreTypes...)
}
}
}

View File

@@ -11,38 +11,13 @@ import (
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/domain"
"github.com/zitadel/zitadel/internal/query"
settings "github.com/zitadel/zitadel/pkg/grpc/settings/v2alpha"
)
var ignoreMessageTypes = map[protoreflect.FullName]bool{
"google.protobuf.Duration": true,
}
// allFieldsSet recusively checks if all values in a message
// have a non-zero value.
func allFieldsSet(t testing.TB, msg protoreflect.Message) {
md := msg.Descriptor()
name := md.FullName()
if ignoreMessageTypes[name] {
return
}
fields := md.Fields()
for i := 0; i < fields.Len(); i++ {
fd := fields.Get(i)
if !msg.Has(fd) {
t.Errorf("not all fields set in %q, missing %q", name, fd.Name())
continue
}
if fd.Kind() == protoreflect.MessageKind {
allFieldsSet(t, msg.Get(fd).Message())
}
}
}
var ignoreTypes = []protoreflect.FullName{"google.protobuf.Duration"}
func Test_loginSettingsToPb(t *testing.T) {
arg := &query.LoginPolicy{
@@ -100,7 +75,7 @@ func Test_loginSettingsToPb(t *testing.T) {
}
got := loginSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("loginSettingsToPb() =\n%v\nwant\n%v", got, want)
}
@@ -241,7 +216,7 @@ func Test_passwordSettingsToPb(t *testing.T) {
}
got := passwordSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("passwordSettingsToPb() =\n%v\nwant\n%v", got, want)
}
@@ -295,7 +270,7 @@ func Test_brandingSettingsToPb(t *testing.T) {
}
got := brandingSettingsToPb(arg, "http://example.com")
allFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("brandingSettingsToPb() =\n%v\nwant\n%v", got, want)
}
@@ -315,7 +290,7 @@ func Test_domainSettingsToPb(t *testing.T) {
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := domainSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("domainSettingsToPb() =\n%v\nwant\n%v", got, want)
}
@@ -337,7 +312,7 @@ func Test_legalSettingsToPb(t *testing.T) {
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := legalAndSupportSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("legalSettingsToPb() =\n%v\nwant\n%v", got, want)
}
@@ -353,7 +328,7 @@ func Test_lockoutSettingsToPb(t *testing.T) {
ResourceOwnerType: settings.ResourceOwnerType_RESOURCE_OWNER_TYPE_INSTANCE,
}
got := lockoutSettingsToPb(arg)
allFieldsSet(t, got.ProtoReflect())
grpc.AllFieldsSet(t, got.ProtoReflect(), ignoreTypes...)
if !proto.Equal(got, want) {
t.Errorf("lockoutSettingsToPb() =\n%v\nwant\n%v", got, want)
}
@@ -387,7 +362,7 @@ func Test_identityProvidersToPb(t *testing.T) {
got := identityProvidersToPb(arg)
require.Len(t, got, len(got))
for i, v := range got {
allFieldsSet(t, v.ProtoReflect())
grpc.AllFieldsSet(t, v.ProtoReflect(), ignoreTypes...)
if !proto.Equal(v, want[i]) {
t.Errorf("identityProvidersToPb() =\n%v\nwant\n%v", got, want)
}

View File

@@ -0,0 +1,104 @@
package user
import (
"context"
"github.com/zitadel/zitadel/internal/api/authz"
"github.com/zitadel/zitadel/internal/api/grpc/object/v2"
"github.com/zitadel/zitadel/internal/domain"
caos_errs "github.com/zitadel/zitadel/internal/errors"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func (s *Server) RegisterPasskey(ctx context.Context, req *user.RegisterPasskeyRequest) (resp *user.RegisterPasskeyResponse, err error) {
var (
resourceOwner = authz.GetCtxData(ctx).ResourceOwner
authenticator = passkeyAuthenticatorToDomain(req.GetAuthenticator())
)
if code := req.GetCode(); code != nil {
return passkeyRegistrationDetailsToPb(
s.command.RegisterUserPasskeyWithCode(ctx, req.GetUserId(), resourceOwner, authenticator, code.Id, code.Code, s.userCodeAlg),
)
}
return passkeyRegistrationDetailsToPb(
s.command.RegisterUserPasskey(ctx, req.GetUserId(), resourceOwner, authenticator),
)
}
func passkeyAuthenticatorToDomain(pa user.PasskeyAuthenticator) domain.AuthenticatorAttachment {
switch pa {
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED:
return domain.AuthenticatorAttachmentUnspecified
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM:
return domain.AuthenticatorAttachmentPlattform
case user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM:
return domain.AuthenticatorAttachmentCrossPlattform
default:
return domain.AuthenticatorAttachmentUnspecified
}
}
func passkeyRegistrationDetailsToPb(details *domain.PasskeyRegistrationDetails, err error) (*user.RegisterPasskeyResponse, error) {
if err != nil {
return nil, err
}
return &user.RegisterPasskeyResponse{
Details: object.DomainToDetailsPb(details.ObjectDetails),
PasskeyId: details.PasskeyID,
PublicKeyCredentialCreationOptions: details.PublicKeyCredentialCreationOptions,
}, nil
}
func (s *Server) VerifyPasskeyRegistration(ctx context.Context, req *user.VerifyPasskeyRegistrationRequest) (*user.VerifyPasskeyRegistrationResponse, error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
objectDetails, err := s.command.HumanHumanPasswordlessSetup(ctx, req.GetUserId(), resourceOwner, req.GetPasskeyName(), "", req.GetPublicKeyCredential())
if err != nil {
return nil, err
}
return &user.VerifyPasskeyRegistrationResponse{
Details: object.DomainToDetailsPb(objectDetails),
}, nil
}
func (s *Server) CreatePasskeyRegistrationLink(ctx context.Context, req *user.CreatePasskeyRegistrationLinkRequest) (resp *user.CreatePasskeyRegistrationLinkResponse, err error) {
resourceOwner := authz.GetCtxData(ctx).ResourceOwner
switch medium := req.Medium.(type) {
case nil:
return passkeyDetailsToPb(
s.command.AddUserPasskeyCode(ctx, req.GetUserId(), resourceOwner, s.userCodeAlg),
)
case *user.CreatePasskeyRegistrationLinkRequest_SendLink:
return passkeyDetailsToPb(
s.command.AddUserPasskeyCodeURLTemplate(ctx, req.GetUserId(), resourceOwner, s.userCodeAlg, medium.SendLink.GetUrlTemplate()),
)
case *user.CreatePasskeyRegistrationLinkRequest_ReturnCode:
return passkeyCodeDetailsToPb(
s.command.AddUserPasskeyCodeReturn(ctx, req.GetUserId(), resourceOwner, s.userCodeAlg),
)
default:
return nil, caos_errs.ThrowUnimplementedf(nil, "USERv2-gaD8y", "verification oneOf %T in method CreatePasskeyRegistrationLink not implemented", medium)
}
}
func passkeyDetailsToPb(details *domain.ObjectDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
if err != nil {
return nil, err
}
return &user.CreatePasskeyRegistrationLinkResponse{
Details: object.DomainToDetailsPb(details),
}, nil
}
func passkeyCodeDetailsToPb(details *domain.PasskeyCodeDetails, err error) (*user.CreatePasskeyRegistrationLinkResponse, error) {
if err != nil {
return nil, err
}
return &user.CreatePasskeyRegistrationLinkResponse{
Details: object.DomainToDetailsPb(details.ObjectDetails),
Code: &user.PasskeyRegistrationCode{
Id: details.CodeID,
Code: details.Code,
},
}, nil
}

View File

@@ -0,0 +1,309 @@
//go:build integration
package user_test
import (
"context"
"testing"
"github.com/muhlemmer/gu"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zitadel/zitadel/internal/integration"
"github.com/zitadel/zitadel/internal/webauthn"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func TestServer_RegisterPasskey(t *testing.T) {
userID := createHumanUser(t).GetUserId()
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
require.NoError(t, err)
client := webauthn.NewClient(Tester.Config.WebAuthNName, Tester.Config.ExternalDomain, "https://"+Tester.Host())
type args struct {
ctx context.Context
req *user.RegisterPasskeyRequest
}
tests := []struct {
name string
args args
want *user.RegisterPasskeyResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{},
},
wantErr: true,
},
{
name: "register code",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
},
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "reuse code (not allowed)",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
},
},
wantErr: true,
},
{
name: "wrong code",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
Code: &user.PasskeyRegistrationCode{
Id: reg.GetCode().GetId(),
Code: "foobar",
},
Authenticator: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
},
},
wantErr: true,
},
{
name: "user mismatch",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: userID,
},
},
wantErr: true,
},
/* TODO after we are able to obtain a Bearer token for a human user
{
name: "human user",
args: args{
ctx: CTX,
req: &user.RegisterPasskeyRequest{
UserId: humanUserID,
},
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
*/
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.RegisterPasskey(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
if tt.want != nil {
assert.NotEmpty(t, got.GetPasskeyId())
assert.NotEmpty(t, got.GetPublicKeyCredentialCreationOptions())
_, err := client.CreateAttestationResponse(got.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
}
})
}
}
func TestServer_VerifyPasskeyRegistration(t *testing.T) {
userID := createHumanUser(t).GetUserId()
reg, err := Client.CreatePasskeyRegistrationLink(CTX, &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
})
require.NoError(t, err)
pkr, err := Client.RegisterPasskey(CTX, &user.RegisterPasskeyRequest{
UserId: userID,
Code: reg.GetCode(),
})
require.NoError(t, err)
require.NotEmpty(t, pkr.GetPasskeyId())
require.NotEmpty(t, pkr.GetPublicKeyCredentialCreationOptions())
client := webauthn.NewClient(Tester.Config.WebAuthNName, Tester.Config.ExternalDomain, "https://"+Tester.Host())
attestationResponse, err := client.CreateAttestationResponse(pkr.GetPublicKeyCredentialCreationOptions())
require.NoError(t, err)
type args struct {
ctx context.Context
req *user.VerifyPasskeyRegistrationRequest
}
tests := []struct {
name string
args args
want *user.VerifyPasskeyRegistrationResponse
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.VerifyPasskeyRegistrationRequest{
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: []byte(attestationResponse),
PasskeyName: "nice name",
},
},
wantErr: true,
},
{
name: "success",
args: args{
ctx: CTX,
req: &user.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: attestationResponse,
PasskeyName: "nice name",
},
},
want: &user.VerifyPasskeyRegistrationResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "wrong credential",
args: args{
ctx: CTX,
req: &user.VerifyPasskeyRegistrationRequest{
UserId: userID,
PasskeyId: pkr.GetPasskeyId(),
PublicKeyCredential: []byte("attestationResponseattestationResponseattestationResponse"),
PasskeyName: "nice name",
},
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.VerifyPasskeyRegistration(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
})
}
}
func TestServer_CreatePasskeyRegistrationLink(t *testing.T) {
userID := createHumanUser(t).GetUserId()
type args struct {
ctx context.Context
req *user.CreatePasskeyRegistrationLinkRequest
}
tests := []struct {
name string
args args
want *user.CreatePasskeyRegistrationLinkResponse
wantCode bool
wantErr bool
}{
{
name: "missing user id",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{},
},
wantErr: true,
},
{
name: "send default mail",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
},
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "send custom url",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_SendLink{
SendLink: &user.SendPasskeyRegistrationLink{
UrlTemplate: gu.Ptr("https://example.com/passkey/register?userID={{.UserID}}&orgID={{.OrgID}}&codeID={{.CodeID}}&code={{.Code}}"),
},
},
},
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
},
{
name: "return code",
args: args{
ctx: CTX,
req: &user.CreatePasskeyRegistrationLinkRequest{
UserId: userID,
Medium: &user.CreatePasskeyRegistrationLinkRequest_ReturnCode{},
},
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
ResourceOwner: Tester.Organisation.ID,
},
},
wantCode: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Client.CreatePasskeyRegistrationLink(tt.args.ctx, tt.args.req)
if tt.wantErr {
require.Error(t, err)
return
}
require.NoError(t, err)
require.NotNil(t, got)
integration.AssertDetails(t, tt.want, got)
if tt.wantCode {
assert.NotEmpty(t, got.GetCode().GetId())
assert.NotEmpty(t, got.GetCode().GetId())
}
})
}
}

View File

@@ -0,0 +1,210 @@
package user
import (
"io"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/timestamppb"
"github.com/zitadel/zitadel/internal/api/grpc"
"github.com/zitadel/zitadel/internal/domain"
object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha"
user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha"
)
func Test_passkeyAuthenticatorToDomain(t *testing.T) {
tests := []struct {
pa user.PasskeyAuthenticator
want domain.AuthenticatorAttachment
}{
{
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_UNSPECIFIED,
want: domain.AuthenticatorAttachmentUnspecified,
},
{
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_PLATFORM,
want: domain.AuthenticatorAttachmentPlattform,
},
{
pa: user.PasskeyAuthenticator_PASSKEY_AUTHENTICATOR_CROSS_PLATFORM,
want: domain.AuthenticatorAttachmentCrossPlattform,
},
{
pa: 999,
want: domain.AuthenticatorAttachmentUnspecified,
},
}
for _, tt := range tests {
t.Run(tt.pa.String(), func(t *testing.T) {
got := passkeyAuthenticatorToDomain(tt.pa)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passkeyRegistrationDetailsToPb(t *testing.T) {
type args struct {
details *domain.PasskeyRegistrationDetails
err error
}
tests := []struct {
name string
args args
want *user.RegisterPasskeyResponse
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok",
args: args{
details: &domain.PasskeyRegistrationDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
PasskeyID: "123",
PublicKeyCredentialCreationOptions: []byte{1, 2, 3},
},
err: nil,
},
want: &user.RegisterPasskeyResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
PasskeyId: "123",
PublicKeyCredentialCreationOptions: []byte{1, 2, 3},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyRegistrationDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
}
})
}
}
func Test_passkeyDetailsToPb(t *testing.T) {
type args struct {
details *domain.ObjectDetails
err error
}
tests := []struct {
name string
args args
want *user.CreatePasskeyRegistrationLinkResponse
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok",
args: args{
details: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
err: nil,
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
})
}
}
func Test_passkeyCodeDetailsToPb(t *testing.T) {
type args struct {
details *domain.PasskeyCodeDetails
err error
}
tests := []struct {
name string
args args
want *user.CreatePasskeyRegistrationLinkResponse
}{
{
name: "an error",
args: args{
details: nil,
err: io.ErrClosedPipe,
},
},
{
name: "ok",
args: args{
details: &domain.PasskeyCodeDetails{
ObjectDetails: &domain.ObjectDetails{
Sequence: 22,
EventDate: time.Unix(3000, 22),
ResourceOwner: "me",
},
CodeID: "123",
Code: "456",
},
err: nil,
},
want: &user.CreatePasskeyRegistrationLinkResponse{
Details: &object.Details{
Sequence: 22,
ChangeDate: &timestamppb.Timestamp{
Seconds: 3000,
Nanos: 22,
},
ResourceOwner: "me",
},
Code: &user.PasskeyRegistrationCode{
Id: "123",
Code: "456",
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := passkeyCodeDetailsToPb(tt.args.details, tt.args.err)
require.ErrorIs(t, err, tt.args.err)
assert.Equal(t, tt.want, got)
if tt.want != nil {
grpc.AllFieldsSet(t, got.ProtoReflect())
}
})
}
}