From 4b7f5ae186b43317f343a2548d1041e1378e909e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tim=20M=C3=B6hlmann?= Date: Fri, 28 Apr 2023 14:39:53 +0300 Subject: [PATCH] AddHumanUser tests --- build/zitadel/Dockerfile | 2 +- .../admin/information_integration_test.go | 2 +- .../api/grpc/user/v2/user_integration_test.go | 317 ++++++++++++++++++ internal/integration/assert.go | 25 ++ internal/integration/config/zitadel.yaml | 1 - internal/integration/integration.go | 94 ++++++ internal/integration/integration_test.go | 3 +- internal/integration/usertype_string.go | 24 ++ 8 files changed, 463 insertions(+), 5 deletions(-) create mode 100644 internal/api/grpc/user/v2/user_integration_test.go create mode 100644 internal/integration/assert.go create mode 100644 internal/integration/usertype_string.go diff --git a/build/zitadel/Dockerfile b/build/zitadel/Dockerfile index ba2c3cccde..7c0f7bdcad 100644 --- a/build/zitadel/Dockerfile +++ b/build/zitadel/Dockerfile @@ -97,7 +97,7 @@ RUN rm -r cockroach-${COCKROACH_VERSION}.linux-amd64 # Migrations for cockroach-secure RUN go install github.com/rakyll/statik \ - && go test -race -v -coverprofile=profile.cov -coverpkg=./... $(go list ./... | grep -v /operator/) + && go test -race -v -coverprofile=profile.cov $(go list ./... | grep -v /operator/) ####################### ## Go test results diff --git a/internal/api/grpc/admin/information_integration_test.go b/internal/api/grpc/admin/information_integration_test.go index cac2c952aa..8eb9818241 100644 --- a/internal/api/grpc/admin/information_integration_test.go +++ b/internal/api/grpc/admin/information_integration_test.go @@ -20,7 +20,7 @@ var ( func TestMain(m *testing.M) { os.Exit(func() int { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, _, cancel := integration.Contexts(time.Minute) defer cancel() Tester = integration.NewTester(ctx) defer Tester.Done() diff --git a/internal/api/grpc/user/v2/user_integration_test.go b/internal/api/grpc/user/v2/user_integration_test.go new file mode 100644 index 0000000000..157aa00fb2 --- /dev/null +++ b/internal/api/grpc/user/v2/user_integration_test.go @@ -0,0 +1,317 @@ +//go:build integration + +package user_test + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + "github.com/muhlemmer/gu" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/internal/integration" + object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" + user "github.com/zitadel/zitadel/pkg/grpc/user/v2alpha" + "google.golang.org/protobuf/types/known/timestamppb" +) + +var ( + CTX context.Context + ErrCTX context.Context + Tester *integration.Tester + Client user.UserServiceClient +) + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, errCtx, cancel := integration.Contexts(time.Hour) + defer cancel() + + Tester = integration.NewTester(ctx) + defer Tester.Done() + + CTX, ErrCTX = Tester.WithSystemAuthorization(ctx, integration.OrgOwner), errCtx + Client = user.NewUserServiceClient(Tester.GRPCClientConn) + return m.Run() + }()) +} + +func TestServer_AddHumanUser(t *testing.T) { + type args struct { + ctx context.Context + req *user.AddHumanUserRequest + } + tests := []struct { + name string + args args + want *user.AddHumanUserResponse + wantErr bool + }{ + { + name: "default verification", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: "211137963315232910", + }, + }, + Profile: &user.SetHumanProfile{ + FirstName: "Donald", + LastName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{}, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "return verification code", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: "211137963315232910", + }, + }, + Profile: &user.SetHumanProfile{ + FirstName: "Donald", + LastName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + EmailCode: gu.Ptr("something"), + }, + }, + { + name: "custom template", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: "211137963315232910", + }, + }, + Profile: &user.SetHumanProfile{ + FirstName: "Donald", + LastName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("https://example.com/email/verify?userID={{.UserID}}&code={{.Code}}&orgID={{.OrgID}}"), + }, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + want: &user.AddHumanUserResponse{ + Details: &object.Details{ + ChangeDate: timestamppb.Now(), + ResourceOwner: Tester.Organisation.ID, + }, + }, + }, + { + name: "custom template error", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: "211137963315232910", + }, + }, + Profile: &user.SetHumanProfile{ + FirstName: "Donald", + LastName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_SendCode{ + SendCode: &user.SendEmailVerificationCode{ + UrlTemplate: gu.Ptr("{{"), + }, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "missing REQUIRED profile", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: "211137963315232910", + }, + }, + Email: &user.SetHumanEmail{ + Verification: &user.SetHumanEmail_ReturnCode{ + ReturnCode: &user.ReturnEmailVerificationCode{}, + }, + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "missing REQUIRED email", + args: args{ + CTX, + &user.AddHumanUserRequest{ + Organisation: &object.Organisation{ + Org: &object.Organisation_OrgId{ + OrgId: "211137963315232910", + }, + }, + Profile: &user.SetHumanProfile{ + FirstName: "Donald", + LastName: "Duck", + NickName: gu.Ptr("Dukkie"), + DisplayName: gu.Ptr("Donald Duck"), + PreferredLanguage: gu.Ptr("en"), + Gender: user.Gender_GENDER_DIVERSE.Enum(), + }, + Metadata: []*user.SetMetadataEntry{ + { + Key: "somekey", + Value: []byte("somevalue"), + }, + }, + PasswordType: &user.AddHumanUserRequest_Password{ + Password: &user.Password{ + Password: "DifficultPW666!", + ChangeRequired: true, + }, + }, + }, + }, + wantErr: true, + }, + } + for i, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + userID := fmt.Sprint(time.Now().UnixNano() + int64(i)) + tt.args.req.UserId = &userID + if email := tt.args.req.GetEmail(); email != nil { + email.Email = fmt.Sprintf("%s@me.now", userID) + } + + if tt.want != nil { + tt.want.UserId = userID + } + + got, err := Client.AddHumanUser(tt.args.ctx, tt.args.req) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + } + + assert.Equal(t, tt.want.GetUserId(), got.GetUserId()) + if tt.want.GetEmailCode() != "" { + assert.NotEmpty(t, got.GetEmailCode()) + } + integration.AssertDetails(t, tt.want, got) + }) + } +} diff --git a/internal/integration/assert.go b/internal/integration/assert.go new file mode 100644 index 0000000000..512c006ad9 --- /dev/null +++ b/internal/integration/assert.go @@ -0,0 +1,25 @@ +package integration + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + + object "github.com/zitadel/zitadel/pkg/grpc/object/v2alpha" +) + +type DetailsMsg interface { + GetDetails() *object.Details +} + +func AssertDetails[D DetailsMsg](t testing.TB, exptected, actual D) { + wantDetails, gotDetails := exptected.GetDetails(), actual.GetDetails() + + if wantDetails != nil { + assert.NotZero(t, gotDetails.GetSequence()) + } + wantCD, gotCD := wantDetails.GetChangeDate().AsTime(), gotDetails.GetChangeDate().AsTime() + assert.WithinRange(t, gotCD, wantCD, wantCD.Add(time.Minute)) + assert.Equal(t, wantDetails.GetResourceOwner(), gotDetails.GetResourceOwner()) +} diff --git a/internal/integration/config/zitadel.yaml b/internal/integration/config/zitadel.yaml index de4eb8aa1d..60389d6de0 100644 --- a/internal/integration/config/zitadel.yaml +++ b/internal/integration/config/zitadel.yaml @@ -35,4 +35,3 @@ Projections: DefaultInstance: LoginPolicy: MfaInitSkipLifetime: "0" - diff --git a/internal/integration/integration.go b/internal/integration/integration.go index f594bb743b..545066f056 100644 --- a/internal/integration/integration.go +++ b/internal/integration/integration.go @@ -4,7 +4,9 @@ package integration import ( "bytes" "context" + "database/sql" _ "embed" + "errors" "fmt" "os" "strings" @@ -13,11 +15,20 @@ import ( "github.com/spf13/viper" "github.com/zitadel/logging" + "github.com/zitadel/oidc/v2/pkg/oidc" "google.golang.org/grpc" "google.golang.org/grpc/credentials/insecure" + "google.golang.org/grpc/metadata" "github.com/zitadel/zitadel/cmd" "github.com/zitadel/zitadel/cmd/start" + "github.com/zitadel/zitadel/internal/api/authz" + z_oidc "github.com/zitadel/zitadel/internal/api/oidc" + "github.com/zitadel/zitadel/internal/command" + "github.com/zitadel/zitadel/internal/domain" + caos_errs "github.com/zitadel/zitadel/internal/errors" + "github.com/zitadel/zitadel/internal/eventstore/v1/models" + "github.com/zitadel/zitadel/internal/query" "github.com/zitadel/zitadel/pkg/grpc/admin" ) @@ -30,8 +41,26 @@ var ( postgresYAML []byte ) +type UserType int + +//go:generate stringer -type=UserType +const ( + Unspecified UserType = iota + OrgOwner +) + +type User struct { + *query.User + Token string +} + type Tester struct { *start.Server + + Instance authz.Instance + Organisation *query.Org + Users map[UserType]User + GRPCClientConn *grpc.ClientConn wg sync.WaitGroup // used for shutdown } @@ -83,6 +112,63 @@ func (s *Tester) pollHealth(ctx context.Context) (err error) { } } +const ( + SystemUser = "integration1" +) + +func (s *Tester) createSystemUser(ctx context.Context) { + var err error + + s.Instance, err = s.Queries.InstanceByHost(ctx, "localhost:8080") + logging.OnError(err).Fatal("query instance") + ctx = authz.WithInstance(ctx, s.Instance) + + s.Organisation, err = s.Queries.OrgByID(ctx, true, s.Instance.DefaultOrganisationID()) + logging.OnError(err).Fatal("query organisation") + + query, err := query.NewUserUsernameSearchQuery(SystemUser, query.TextEquals) + logging.OnError(err).Fatal("user query") + user, err := s.Queries.GetUser(ctx, true, true, query) + + if errors.Is(err, sql.ErrNoRows) { + _, err = s.Commands.AddMachine(ctx, &command.Machine{ + ObjectRoot: models.ObjectRoot{ + ResourceOwner: s.Organisation.ID, + }, + Username: SystemUser, + Name: SystemUser, + Description: "who cares?", + AccessTokenType: domain.OIDCTokenTypeJWT, + }) + logging.OnError(err).Fatal("add machine user") + user, err = s.Queries.GetUser(ctx, true, true, query) + + } + logging.OnError(err).Fatal("get user") + + _, err = s.Commands.AddOrgMember(ctx, s.Organisation.ID, user.ID, "ORG_OWNER") + target := new(caos_errs.AlreadyExistsError) + if !errors.As(err, &target) { + logging.OnError(err).Fatal("add org member") + } + + scopes := []string{oidc.ScopeOpenID, z_oidc.ScopeUserMetaData, z_oidc.ScopeResourceOwner} + pat := command.NewPersonalAccessToken(user.ResourceOwner, user.ID, time.Now().Add(time.Hour), scopes, domain.UserTypeMachine) + _, err = s.Commands.AddPersonalAccessToken(ctx, pat) + logging.OnError(err).Fatal("add pat") + + s.Users = map[UserType]User{ + OrgOwner: { + User: user, + Token: pat.Token, + }, + } +} + +func (s *Tester) WithSystemAuthorization(ctx context.Context, u UserType) context.Context { + return metadata.AppendToOutgoingContext(ctx, "Authorization", fmt.Sprintf("Bearer %s", s.Users[u].Token)) +} + func (s *Tester) Done() { err := s.GRPCClientConn.Close() logging.OnError(err).Error("integration tester client close") @@ -125,6 +211,14 @@ func NewTester(ctx context.Context) *Tester { logging.OnError(ctx.Err()).Fatal("waiting for integration tester server") } tester.createClientConn(ctx) + tester.createSystemUser(ctx) return tester } + +func Contexts(timeout time.Duration) (ctx, errCtx context.Context, cancel context.CancelFunc) { + errCtx, cancel = context.WithCancel(context.Background()) + cancel() + ctx, cancel = context.WithTimeout(context.Background(), timeout) + return ctx, errCtx, cancel +} diff --git a/internal/integration/integration_test.go b/internal/integration/integration_test.go index a01d8cc7ff..416602ea25 100644 --- a/internal/integration/integration_test.go +++ b/internal/integration/integration_test.go @@ -3,13 +3,12 @@ package integration import ( - "context" "testing" "time" ) func TestNewTester(t *testing.T) { - ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + ctx, _, cancel := Contexts(time.Hour) defer cancel() s := NewTester(ctx) diff --git a/internal/integration/usertype_string.go b/internal/integration/usertype_string.go new file mode 100644 index 0000000000..3f5db98d72 --- /dev/null +++ b/internal/integration/usertype_string.go @@ -0,0 +1,24 @@ +// Code generated by "stringer -type=UserType"; DO NOT EDIT. + +package integration + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Unspecified-0] + _ = x[OrgOwner-1] +} + +const _UserType_name = "UnspecifiedOrgOwner" + +var _UserType_index = [...]uint8{0, 11, 19} + +func (i UserType) String() string { + if i < 0 || i >= UserType(len(_UserType_index)-1) { + return "UserType(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _UserType_name[_UserType_index[i]:_UserType_index[i+1]] +}