From 8ea414e335429f4b11663967befe86322f1a06a9 Mon Sep 17 00:00:00 2001 From: Iraq Jaber Date: Wed, 28 May 2025 18:18:50 +0200 Subject: [PATCH] started adding tests --- backend/v3/domain/instance.go | 14 +- .../storage/database/dialect/postgres/pool.go | 10 +- .../database/events_testing/instance_test.go | 117 +++++++++++++ .../storage/database/repository/instance.go | 159 ++++++++++++++++++ .../database/repository/instance_test.go | 101 +++++++++++ .../storage/database/repository/org_test.go | 2 +- .../storage/database/repository/repository.go | 1 + .../database/repository/repository_test.go | 2 +- .../query/projection/instance_relational.go | 1 + 9 files changed, 396 insertions(+), 11 deletions(-) create mode 100644 backend/v3/storage/database/events_testing/instance_test.go create mode 100644 backend/v3/storage/database/repository/instance.go create mode 100644 backend/v3/storage/database/repository/instance_test.go diff --git a/backend/v3/domain/instance.go b/backend/v3/domain/instance.go index 1ff12c9d0a..5c4782461c 100644 --- a/backend/v3/domain/instance.go +++ b/backend/v3/domain/instance.go @@ -2,6 +2,7 @@ package domain import ( "context" + "database/sql" "time" "github.com/zitadel/zitadel/backend/v3/storage/cache" @@ -9,11 +10,11 @@ import ( ) type Instance struct { - ID string `json:"id"` - Name string `json:"name"` - CreatedAt time.Time `json:"-"` - UpdatedAt time.Time `json:"-"` - DeletedAt time.Time `json:"-"` + ID string `json:"id"` + Name string `json:"name"` + CreatedAt sql.Null[time.Time] `json:"-"` + UpdatedAt sql.Null[time.Time] `json:"-"` + DeletedAt sql.Null[time.Time] `json:"-"` } type instanceCacheIndex uint8 @@ -67,8 +68,9 @@ type InstanceRepository interface { instanceConditions instanceChanges + // TODO // Member returns the member repository which is a sub repository of the instance repository. - Member() MemberRepository + // Member() MemberRepository Get(ctx context.Context, opts ...database.QueryOption) (*Instance, error) diff --git a/backend/v3/storage/database/dialect/postgres/pool.go b/backend/v3/storage/database/dialect/postgres/pool.go index e3006d91eb..e79ef9fec8 100644 --- a/backend/v3/storage/database/dialect/postgres/pool.go +++ b/backend/v3/storage/database/dialect/postgres/pool.go @@ -13,9 +13,13 @@ type pgxPool struct { *pgxpool.Pool } -var ( - _ database.Pool = (*pgxPool)(nil) -) +var _ database.Pool = (*pgxPool)(nil) + +func PGxPool(pool *pgxpool.Pool) *pgxPool { + return &pgxPool{ + Pool: pool, + } +} // Acquire implements [database.Pool]. func (c *pgxPool) Acquire(ctx context.Context) (database.Client, error) { diff --git a/backend/v3/storage/database/events_testing/instance_test.go b/backend/v3/storage/database/events_testing/instance_test.go new file mode 100644 index 0000000000..c2d57190b1 --- /dev/null +++ b/backend/v3/storage/database/events_testing/instance_test.go @@ -0,0 +1,117 @@ +//go:build integration + +package instance_test + +import ( + "context" + "os" + "testing" + "time" + + "github.com/brianvoe/gofakeit/v6" + "github.com/jackc/pgx/v5/pgxpool" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + "github.com/zitadel/zitadel/internal/integration" + "github.com/zitadel/zitadel/pkg/grpc/system" +) + +const ConnString = "host=localhost port=5432 user=zitadel dbname=zitadel sslmode=disable" + +var ( + dbPool *pgxpool.Pool + CTX context.Context + SystemCTX context.Context + Instance *integration.Instance + SystemClient system.SystemServiceClient +) + +var pool database.Pool + +func TestMain(m *testing.M) { + os.Exit(func() int { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + Instance = integration.NewInstance(ctx) + + CTX = Instance.WithAuthorization(ctx, integration.UserTypeIAMOwner) + // SystemCTX = integration.WithSystemAuthorization(ctx) + SystemClient = integration.SystemClient() + + var err error + dbPool, err = pgxpool.New(context.Background(), ConnString) + if err != nil { + panic(err) + } + + pool = postgres.PGxPool(dbPool) + + return m.Run() + }()) +} + +func TestServer_TestInstanceAddReduces(t *testing.T) { + instanceName := "newInstance" + _, err := SystemClient.CreateInstance(CTX, &system.CreateInstanceRequest{ + InstanceName: instanceName, + Owner: &system.CreateInstanceRequest_Machine_{ + Machine: &system.CreateInstanceRequest_Machine{ + UserName: "owner", + Name: "owner", + PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{}, + }, + }, + }) + + require.NoError(t, err) + + instanceRepo := repository.InstanceRepository(pool) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + instance, err := instanceRepo.Get(CTX, + database.WithCondition( + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), + ), + ) + require.NoError(ttt, err) + require.Equal(ttt, instanceName, instance.Name) + }, retryDuration, tick) +} + +func TestServer_TestInstanceUpdateNameReduces(t *testing.T) { + instanceName := gofakeit.Name() + res, err := SystemClient.CreateInstance(CTX, &system.CreateInstanceRequest{ + InstanceName: instanceName, + Owner: &system.CreateInstanceRequest_Machine_{ + Machine: &system.CreateInstanceRequest_Machine{ + UserName: "owner", + Name: "owner", + PersonalAccessToken: &system.CreateInstanceRequest_PersonalAccessToken{}, + }, + }, + }) + require.NoError(t, err) + + instanceName += "new" + _, err = SystemClient.UpdateInstance(CTX, &system.UpdateInstanceRequest{ + InstanceId: res.InstanceId, + InstanceName: instanceName, + }) + require.NoError(t, err) + + instanceRepo := repository.InstanceRepository(pool) + retryDuration, tick := integration.WaitForAndTickWithMaxDuration(CTX, time.Minute) + assert.EventuallyWithT(t, func(ttt *assert.CollectT) { + instance, err := instanceRepo.Get(CTX, + database.WithCondition( + instanceRepo.NameCondition(database.TextOperationEqual, instanceName), + ), + ) + require.NoError(ttt, err) + require.Equal(ttt, instanceName, instance.Name) + }, retryDuration, tick) +} diff --git a/backend/v3/storage/database/repository/instance.go b/backend/v3/storage/database/repository/instance.go new file mode 100644 index 0000000000..4f4369f38c --- /dev/null +++ b/backend/v3/storage/database/repository/instance.go @@ -0,0 +1,159 @@ +package repository + +import ( + "context" + "errors" + + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" +) + +var _ domain.InstanceRepository = (*instance)(nil) + +type instance struct { + repository +} + +func InstanceRepository(client database.QueryExecutor) domain.InstanceRepository { + return &instance{ + repository: repository{ + client: client, + }, + } +} + +// ------------------------------------------------------------- +// repository +// ------------------------------------------------------------- + +const queryInstanceStmt = `SELECT id, name, created_at, updated_at, deleted_at` + + ` FROM zitadel.instances` + +// Get implements [domain.InstanceRepository]. +func (i *instance) Get(ctx context.Context, opts ...database.QueryOption) (*domain.Instance, error) { + i.builder = database.StatementBuilder{} + options := new(database.QueryOpts) + for _, opt := range opts { + opt(options) + } + + i.builder.WriteString(queryInstanceStmt) + options.WriteCondition(&i.builder) + options.WriteOrderBy(&i.builder) + options.WriteLimit(&i.builder) + options.WriteOffset(&i.builder) + + return scanInstance(i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...)) +} + +const createInstanceStmt = `INSERT INTO zitadel.instances (id, name)` + + ` VALUES ($1, $2)` + + ` RETURNING created_at, updated_at` + +// Create implements [domain.InstanceRepository]. +func (i *instance) Create(ctx context.Context, instance *domain.Instance) error { + i.builder = database.StatementBuilder{} + i.builder.AppendArgs(instance.ID, instance.Name) + i.builder.WriteString(createInstanceStmt) + + return i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...).Scan(&instance.CreatedAt, &instance.UpdatedAt) +} + +// Update implements [domain.InstanceRepository]. +func (i instance) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error { + i.builder = database.StatementBuilder{} + i.builder.WriteString(`UPDATE human_users SET `) + database.Changes(changes).Write(&i.builder) + i.writeCondition(condition) + + stmt := i.builder.String() + + return i.client.Exec(ctx, stmt, i.builder.Args()...) +} + +// Delete implements [domain.InstanceRepository]. +func (i instance) Delete(ctx context.Context, condition database.Condition) error { + i.builder.WriteString("DELETE FROM instance") + + if condition == nil { + return errors.New("Delete must contain a condition") // (otherwise ALL instances will be deleted) + } + i.writeCondition(condition) + return i.client.Exec(ctx, i.builder.String(), i.builder.Args()...) +} + +// ------------------------------------------------------------- +// changes +// ------------------------------------------------------------- + +// SetName implements [domain.instanceChanges]. +func (i instance) SetName(name string) database.Change { + return database.NewChange(i.NameColumn(), name) +} + +// ------------------------------------------------------------- +// conditions +// ------------------------------------------------------------- + +// IDCondition implements [domain.instanceConditions]. +func (i instance) IDCondition(id string) database.Condition { + return database.NewTextCondition(i.IDColumn(), database.TextOperationEqual, id) +} + +// NameCondition implements [domain.instanceConditions]. +func (i instance) NameCondition(op database.TextOperation, name string) database.Condition { + return database.NewTextCondition(i.NameColumn(), op, name) +} + +// ------------------------------------------------------------- +// columns +// ------------------------------------------------------------- + +// IDColumn implements [domain.instanceColumns]. +func (instance) IDColumn() database.Column { + return database.NewColumn("id") +} + +// NameColumn implements [domain.instanceColumns]. +func (instance) NameColumn() database.Column { + return database.NewColumn("name") +} + +// CreatedAtColumn implements [domain.instanceColumns]. +func (instance) CreatedAtColumn() database.Column { + return database.NewColumn("created_at") +} + +// UpdatedAtColumn implements [domain.instanceColumns]. +func (instance) UpdatedAtColumn() database.Column { + return database.NewColumn("updated_at") +} + +// DeletedAtColumn implements [domain.instanceColumns]. +func (instance) DeletedAtColumn() database.Column { + return database.NewColumn("deleted_at") +} + +func (i *instance) writeCondition(condition database.Condition) { + if condition == nil { + return + } + i.builder.WriteString(" WHERE ") + condition.Write(&i.builder) +} + +func scanInstance(scanner database.Scanner) (*domain.Instance, error) { + var instance domain.Instance + err := scanner.Scan( + &instance.ID, + &instance.Name, + &instance.CreatedAt, + &instance.UpdatedAt, + &instance.DeletedAt, + ) + if err != nil { + return nil, err + } + + return &instance, nil +} diff --git a/backend/v3/storage/database/repository/instance_test.go b/backend/v3/storage/database/repository/instance_test.go new file mode 100644 index 0000000000..4fac9335ec --- /dev/null +++ b/backend/v3/storage/database/repository/instance_test.go @@ -0,0 +1,101 @@ +package repository_test + +import ( + "context" + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/zitadel/zitadel/backend/v3/domain" + "github.com/zitadel/zitadel/backend/v3/storage/database" + "github.com/zitadel/zitadel/backend/v3/storage/database/dbmock" + "github.com/zitadel/zitadel/backend/v3/storage/database/repository" + + "go.uber.org/mock/gomock" +) + +func TestCreateInstance(t *testing.T) { + instanceRepo := repository.InstanceRepository(pool) + + ctx := context.Background() + inst := domain.Instance{ + ID: "id", + Name: "name", + } + err := instanceRepo.Create(ctx, &inst) + fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> err = %+v\n", err) + fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> inst = %+v\n", inst) + require.NoError(t, err) + + instt, err := instanceRepo.Get(ctx, + database.WithCondition( + instanceRepo.NameCondition(database.TextOperationEqual, "name"), + ), + ) + // require.NoError(t, err) + fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> err = %+v\n", err) + fmt.Printf("@@ >>>>>>>>>>>>>>>>>>>>>>>>>>>> inst = %+v\n", instt) + + t.Skip("tests are meant as examples and are not real tests") + t.Run("User filters", func(t *testing.T) { + client := dbmock.NewMockClient(gomock.NewController(t)) + + user := repository.UserRepository(client) + u, err := user.Get(context.Background(), + database.WithCondition( + database.And( + database.Or( + user.IDCondition("test"), + user.IDCondition("2"), + ), + user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"), + ), + ), + database.WithOrderBy(user.CreatedAtColumn()), + ) + + assert.NoError(t, err) + assert.NotNil(t, u) + }) + + t.Run("machine and human filters", func(t *testing.T) { + client := dbmock.NewMockClient(gomock.NewController(t)) + + user := repository.UserRepository(client) + machine := user.Machine() + human := user.Human() + email, err := human.GetEmail(context.Background(), database.And( + user.UsernameCondition(database.TextOperationStartsWithIgnoreCase, "test"), + database.Or( + machine.DescriptionCondition(database.TextOperationStartsWithIgnoreCase, "test"), + human.EmailVerifiedCondition(true), + database.IsNotNull(machine.DescriptionColumn()), + ), + )) + + assert.NoError(t, err) + assert.NotNil(t, email) + }) +} + +// type dbInstruction string + +// func TestArg(t *testing.T) { +// var bla any = "asdf" +// instr, ok := bla.(dbInstruction) +// assert.False(t, ok) +// assert.Empty(t, instr) +// bla = dbInstruction("asdf") +// instr, ok = bla.(dbInstruction) +// assert.True(t, ok) +// assert.Equal(t, instr, dbInstruction("asdf")) +// } + +// func TestWriteUser(t *testing.T) { +// t.Skip("tests are meant as examples and are not real tests") +// t.Run("update user", func(t *testing.T) { +// user := repository.UserRepository(nil) +// user.Human().Update(context.Background(), user.IDCondition("test"), user.SetUsername("test")) +// }) +// } diff --git a/backend/v3/storage/database/repository/org_test.go b/backend/v3/storage/database/repository/org_test.go index 996e8d1b2c..9593c22fad 100644 --- a/backend/v3/storage/database/repository/org_test.go +++ b/backend/v3/storage/database/repository/org_test.go @@ -1,4 +1,4 @@ -package repository +package repository_test import ( "context" diff --git a/backend/v3/storage/database/repository/repository.go b/backend/v3/storage/database/repository/repository.go index ebd99a66d6..55b3edd7e6 100644 --- a/backend/v3/storage/database/repository/repository.go +++ b/backend/v3/storage/database/repository/repository.go @@ -3,6 +3,7 @@ package repository import "github.com/zitadel/zitadel/backend/v3/storage/database" type repository struct { + // we can't reuse builder after it's been used already, I think we should remove it builder database.StatementBuilder client database.QueryExecutor } diff --git a/backend/v3/storage/database/repository/repository_test.go b/backend/v3/storage/database/repository/repository_test.go index 7cbca2114f..05ec45b930 100644 --- a/backend/v3/storage/database/repository/repository_test.go +++ b/backend/v3/storage/database/repository/repository_test.go @@ -1,4 +1,4 @@ -package repository +package repository_test import ( "context" diff --git a/internal/query/projection/instance_relational.go b/internal/query/projection/instance_relational.go index 5e383a4ea3..8e2d45521c 100644 --- a/internal/query/projection/instance_relational.go +++ b/internal/query/projection/instance_relational.go @@ -33,6 +33,7 @@ func (*instanceRelationalProjection) Init() *old_handler.Check { handler.NewColumn(InstanceColumnProjectID, handler.ColumnTypeText, handler.Default("")), handler.NewColumn(InstanceColumnConsoleID, handler.ColumnTypeText, handler.Default("")), handler.NewColumn(InstanceColumnConsoleAppID, handler.ColumnTypeText, handler.Default("")), + handler.NewColumn(InstanceColumnSequence, handler.ColumnTypeInt64), handler.NewColumn(InstanceColumnDefaultLanguage, handler.ColumnTypeText, handler.Default("")), }, handler.NewPrimaryKey(InstanceColumnID),