diff --git a/backend/v3/domain/instance.go b/backend/v3/domain/instance.go index 49beb3805b..acbee10c1b 100644 --- a/backend/v3/domain/instance.go +++ b/backend/v3/domain/instance.go @@ -9,16 +9,16 @@ import ( ) type Instance struct { - ID string `json:"id"` - Name string `json:"name"` - DefaultOrgID string `json:"default_org_id"` - IAMProjectID string `json:"iam_project_id"` - ConsoleClientId string `json:"console_client_id"` - ConsoleAppID string `json:"console_app_id"` - DefaultLanguage string `json:"default_language"` - CreatedAt time.Time `json:"-"` - UpdatedAt time.Time `json:"-"` - DeletedAt *time.Time `json:"-"` + ID string `json:"id,omitempty" db:"id"` + Name string `json:"name,omitempty" db:"name"` + DefaultOrgID string `json:"default_org_id,omitempty" db:"default_org_id"` + IAMProjectID string `json:"iam_project_id,omitempty" db:"iam_project_id"` + ConsoleClientID string `json:"console_client_id,omitempty" db:"console_client_id"` + ConsoleAppID string `json:"console_app_id,omitempty" db:"console_app_id"` + DefaultLanguage string `json:"default_language,omitempty" db:"default_language"` + CreatedAt time.Time `json:"-,omitempty" db:"created_at"` + UpdatedAt time.Time `json:"-,omitempty" db:"updated_at"` + DeletedAt *time.Time `json:"-,omitempty" db:"deleted_at"` } type instanceCacheIndex uint8 @@ -44,8 +44,8 @@ type instanceColumns interface { IDColumn() database.Column // NameColumn returns the column for the name field. NameColumn() database.Column - // DefaultOrgIdColumn returns the column for the default org id field - DefaultOrgIdColumn() database.Column + // DefaultOrgIDColumn returns the column for the default org id field + DefaultOrgIDColumn() database.Column // IAMProjectIDColumn returns the column for the default IAM org id field IAMProjectIDColumn() database.Column // ConsoleClientIDColumn returns the column for the default IAM org id field @@ -87,9 +87,10 @@ type InstanceRepository interface { // Member() MemberRepository Get(ctx context.Context, opts ...database.Condition) (*Instance, error) + List(ctx context.Context, opts ...database.Condition) ([]*Instance, error) Create(ctx context.Context, instance *Instance) error - Update(ctx context.Context, condition database.Condition, changes ...database.Change) error + Update(ctx context.Context, condition database.Condition, changes ...database.Change) (int64, error) Delete(ctx context.Context, condition database.Condition) error } diff --git a/backend/v3/storage/database/database.go b/backend/v3/storage/database/database.go index f11f67a628..7a50703a25 100644 --- a/backend/v3/storage/database/database.go +++ b/backend/v3/storage/database/database.go @@ -31,7 +31,7 @@ type Querier interface { // Executor is a database client that can execute statements. type Executor interface { - Exec(ctx context.Context, stmt string, args ...any) error + Exec(ctx context.Context, stmt string, args ...any) (int64, error) } // QueryExecutor is a database client that can execute queries and statements. diff --git a/backend/v3/storage/database/dialect/postgres/conn.go b/backend/v3/storage/database/dialect/postgres/conn.go index 0cb5d8f16a..aa477dfd51 100644 --- a/backend/v3/storage/database/dialect/postgres/conn.go +++ b/backend/v3/storage/database/dialect/postgres/conn.go @@ -13,9 +13,7 @@ type pgxConn struct { *pgxpool.Conn } -var ( - _ database.Client = (*pgxConn)(nil) -) +var _ database.Client = (*pgxConn)(nil) // Release implements [database.Client]. func (c *pgxConn) Release(_ context.Context) error { @@ -47,9 +45,9 @@ func (c *pgxConn) QueryRow(ctx context.Context, sql string, args ...any) databas // Exec implements [database.Pool]. // Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Conn.Exec(ctx, sql, args...) - return err +func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) (int64, error) { + res, err := c.Conn.Exec(ctx, sql, args...) + return res.RowsAffected(), err } // Migrate implements [database.Migrator]. diff --git a/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql b/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql index 6cd4a89124..3003bfa2e4 100644 --- a/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql +++ b/backend/v3/storage/database/dialect/postgres/migration/001_instance_table/up.sql @@ -1,6 +1,6 @@ CREATE TABLE IF NOT EXISTS zitadel.instances( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, + id TEXT NOT NULL CHECK (id <> '') PRIMARY KEY, + name TEXT NOT NULL CHECK (name <> ''), default_org_id TEXT, -- NOT NULL, iam_project_id TEXT, -- NOT NULL, console_client_id TEXT, -- NOT NULL, diff --git a/backend/v3/storage/database/dialect/postgres/pool.go b/backend/v3/storage/database/dialect/postgres/pool.go index e79ef9fec8..971285d86b 100644 --- a/backend/v3/storage/database/dialect/postgres/pool.go +++ b/backend/v3/storage/database/dialect/postgres/pool.go @@ -45,9 +45,9 @@ func (c *pgxPool) QueryRow(ctx context.Context, sql string, args ...any) databas // Exec implements [database.Pool]. // Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (c *pgxPool) Exec(ctx context.Context, sql string, args ...any) error { - _, err := c.Pool.Exec(ctx, sql, args...) - return err +func (c *pgxPool) Exec(ctx context.Context, sql string, args ...any) (int64, error) { + res, err := c.Pool.Exec(ctx, sql, args...) + return res.RowsAffected(), err } // Begin implements [database.Pool]. diff --git a/backend/v3/storage/database/dialect/postgres/tx.go b/backend/v3/storage/database/dialect/postgres/tx.go index bfac46572d..f7b0192d39 100644 --- a/backend/v3/storage/database/dialect/postgres/tx.go +++ b/backend/v3/storage/database/dialect/postgres/tx.go @@ -46,9 +46,9 @@ func (tx *pgxTx) QueryRow(ctx context.Context, sql string, args ...any) database // Exec implements [database.Transaction]. // Subtle: this method shadows the method (Pool).Exec of pgxPool.Pool. -func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) error { - _, err := tx.Tx.Exec(ctx, sql, args...) - return err +func (tx *pgxTx) Exec(ctx context.Context, sql string, args ...any) (int64, error) { + res, err := tx.Tx.Exec(ctx, sql, args...) + return res.RowsAffected(), err } // Begin implements [database.Transaction]. diff --git a/backend/v3/storage/database/repository/instance.go b/backend/v3/storage/database/repository/instance.go index 4e01aae6dd..a41b347d4b 100644 --- a/backend/v3/storage/database/repository/instance.go +++ b/backend/v3/storage/database/repository/instance.go @@ -5,6 +5,7 @@ import ( "errors" "time" + "github.com/jackc/pgx/v5/pgconn" "github.com/zitadel/zitadel/backend/v3/domain" "github.com/zitadel/zitadel/backend/v3/storage/database" ) @@ -37,14 +38,33 @@ func (i *instance) Get(ctx context.Context, opts ...database.Condition) (*domain i.builder.WriteString(queryInstanceStmt) - isNotDeletedCondition := database.IsNull(i.DeletedAtColumn()) - opts = append(opts, isNotDeletedCondition) + opts = append(opts, database.IsNull(i.DeletedAtColumn())) andCondition := database.And(opts...) andCondition.Write(&i.builder) return scanInstance(i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...)) } +// List implements [domain.InstanceRepository]. +// func (i *instance) List(ctx context.Context, opts ...database.QueryOption) (*domain.Instance, error) { +func (i *instance) List(ctx context.Context, opts ...database.Condition) ([]*domain.Instance, error) { + i.builder = database.StatementBuilder{} + + i.builder.WriteString(queryInstanceStmt) + + opts = append(opts, database.IsNull(i.DeletedAtColumn())) + andCondition := database.And(opts...) + andCondition.Write(&i.builder) + + rows, err := i.client.Query(ctx, i.builder.String(), i.builder.Args()...) + if err != nil { + return nil, err + } + defer rows.Close() + + return scanInstances(rows) +} + const createInstanceStmt = `INSERT INTO zitadel.instances (id, name, default_org_id, iam_project_id, console_client_id, console_app_id, default_language)` + ` VALUES ($1, $2, $3, $4, $5, $6, $7)` + ` RETURNING created_at, updated_at` @@ -52,14 +72,35 @@ const createInstanceStmt = `INSERT INTO zitadel.instances (id, name, default_org // 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, instance.DefaultOrgID, instance.IAMProjectID, instance.ConsoleClientId, instance.ConsoleAppID, instance.DefaultLanguage) + i.builder.AppendArgs(instance.ID, instance.Name, instance.DefaultOrgID, instance.IAMProjectID, instance.ConsoleClientID, instance.ConsoleAppID, instance.DefaultLanguage) i.builder.WriteString(createInstanceStmt) - return i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...).Scan(&instance.CreatedAt, &instance.UpdatedAt) + err := i.client.QueryRow(ctx, i.builder.String(), i.builder.Args()...).Scan(&instance.CreatedAt, &instance.UpdatedAt) + if err != nil { + var pgErr *pgconn.PgError + if errors.As(err, &pgErr) { + // constraint violation + if pgErr.Code == "23514" { + if pgErr.ConstraintName == "instances_name_check" { + return errors.New("instnace name not provided") + } + if pgErr.ConstraintName == "instances_id_check" { + return errors.New("instnace id not provided") + } + } + // duplicate + if pgErr.Code == "23505" { + if pgErr.ConstraintName == "instances_pkey" { + return errors.New("instnace id already exists") + } + } + } + } + return err } // Update implements [domain.InstanceRepository]. -func (i instance) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error { +func (i instance) Update(ctx context.Context, condition database.Condition, changes ...database.Change) (int64, error) { i.builder = database.StatementBuilder{} i.builder.WriteString(`UPDATE zitadel.instances SET `) database.Changes(changes).Write(&i.builder) @@ -67,7 +108,8 @@ func (i instance) Update(ctx context.Context, condition database.Condition, chan stmt := i.builder.String() - return i.client.Exec(ctx, stmt, i.builder.Args()...) + rowsAffected, err := i.client.Exec(ctx, stmt, i.builder.Args()...) + return rowsAffected, err } // Delete implements [domain.InstanceRepository]. @@ -80,7 +122,8 @@ func (i instance) Delete(ctx context.Context, condition database.Condition) erro i.builder.AppendArgs(time.Now()) i.writeCondition(condition) - return i.client.Exec(ctx, i.builder.String(), i.builder.Args()...) + _, err := i.client.Exec(ctx, i.builder.String(), i.builder.Args()...) + return err } // ------------------------------------------------------------- @@ -126,7 +169,7 @@ func (instance) CreatedAtColumn() database.Column { } // DefaultOrgIdColumn implements [domain.instanceColumns]. -func (instance) DefaultOrgIdColumn() database.Column { +func (instance) DefaultOrgIDColumn() database.Column { return database.NewColumn("default_org_id") } @@ -175,7 +218,7 @@ func scanInstance(scanner database.Scanner) (*domain.Instance, error) { &instance.Name, &instance.DefaultOrgID, &instance.IAMProjectID, - &instance.ConsoleClientId, + &instance.ConsoleClientID, &instance.ConsoleAppID, &instance.DefaultLanguage, &instance.CreatedAt, @@ -194,3 +237,30 @@ func scanInstance(scanner database.Scanner) (*domain.Instance, error) { return &instance, nil } + +func scanInstances(rows database.Rows) ([]*domain.Instance, error) { + instances := make([]*domain.Instance, 0) + for rows.Next() { + + var instance domain.Instance + err := rows.Scan( + &instance.ID, + &instance.Name, + &instance.DefaultOrgID, + &instance.IAMProjectID, + &instance.ConsoleClientID, + &instance.ConsoleAppID, + &instance.DefaultLanguage, + &instance.CreatedAt, + &instance.UpdatedAt, + &instance.DeletedAt, + ) + if err != nil { + return nil, err + } + + instances = append(instances, &instance) + + } + return instances, nil +} diff --git a/backend/v3/storage/database/repository/instance_test.go b/backend/v3/storage/database/repository/instance_test.go index 9a7410b5e9..bfc603ac65 100644 --- a/backend/v3/storage/database/repository/instance_test.go +++ b/backend/v3/storage/database/repository/instance_test.go @@ -2,12 +2,12 @@ package repository_test import ( "context" + "errors" "testing" "time" "github.com/brianvoe/gofakeit/v6" "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" @@ -15,81 +15,363 @@ import ( ) func TestCreateInstance(t *testing.T) { - instanceRepo := repository.InstanceRepository(pool) - instanceId := gofakeit.Name() - instanceName := gofakeit.Name() + tests := []struct { + name string + testFunc func() *domain.Instance + instance domain.Instance + err error + }{ + { + name: "happy path", + instance: func() domain.Instance { + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() + instance := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + return instance + }(), + }, + { + name: "create instance wihtout name", + instance: func() domain.Instance { + instanceId := gofakeit.Name() + // instanceName := gofakeit.Name() + instance := domain.Instance{ + ID: instanceId, + Name: "", + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + return instance + }(), + err: errors.New("instnace name not provided"), + }, + { + name: "adding same instance twice", + testFunc: func() *domain.Instance { + instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() - ctx := context.Background() - inst := domain.Instance{ - ID: instanceId, - Name: instanceName, - DefaultOrgID: "defaultOrgId", - IAMProjectID: "iamProject", - ConsoleClientId: "consoleCLient", - ConsoleAppID: "consoleApp", - DefaultLanguage: "defaultLanguage", + ctx := context.Background() + inst := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + + err := instanceRepo.Create(ctx, &inst) + assert.NoError(t, err) + return &inst + }, + err: errors.New("instnace id already exists"), + }, + { + name: "adding instance with no id", + instance: func() domain.Instance { + // instanceId := gofakeit.Name() + instanceName := gofakeit.Name() + instance := domain.Instance{ + // ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + return instance + }(), + err: errors.New("instnace id not provided"), + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() - beforeCreate := time.Now() - err := instanceRepo.Create(ctx, &inst) - require.NoError(t, err) - afterCreate := time.Now() + var instance *domain.Instance + if tt.testFunc != nil { + instance = tt.testFunc() + } else { + instance = &tt.instance + } + instanceRepo := repository.InstanceRepository(pool) - instance, err := instanceRepo.Get(ctx, - instanceRepo.NameCondition(database.TextOperationEqual, instanceName), - ) - require.Equal(t, inst.ID, instance.ID) - require.Equal(t, inst.Name, instance.Name) - require.Equal(t, inst.DefaultOrgID, instance.DefaultOrgID) - require.Equal(t, inst.IAMProjectID, instance.IAMProjectID) - require.Equal(t, inst.ConsoleClientId, instance.ConsoleClientId) - require.Equal(t, inst.ConsoleAppID, instance.ConsoleAppID) - require.Equal(t, inst.DefaultLanguage, instance.DefaultLanguage) - assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate) - assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate) - require.Nil(t, instance.DeletedAt) - require.NoError(t, err) + // create instance + beforeCreate := time.Now() + err := instanceRepo.Create(ctx, instance) + assert.Equal(t, tt.err, err) + if err != nil { + return + } + afterCreate := time.Now() + + // check instance values + instance, err = instanceRepo.Get(ctx, + instanceRepo.NameCondition(database.TextOperationEqual, instance.Name), + ) + assert.Equal(t, tt.instance.ID, instance.ID) + assert.Equal(t, tt.instance.Name, instance.Name) + assert.Equal(t, tt.instance.DefaultOrgID, instance.DefaultOrgID) + assert.Equal(t, tt.instance.IAMProjectID, instance.IAMProjectID) + assert.Equal(t, tt.instance.ConsoleClientID, instance.ConsoleClientID) + assert.Equal(t, tt.instance.ConsoleAppID, instance.ConsoleAppID) + assert.Equal(t, tt.instance.DefaultLanguage, instance.DefaultLanguage) + assert.WithinRange(t, instance.CreatedAt, beforeCreate, afterCreate) + assert.WithinRange(t, instance.UpdatedAt, beforeCreate, afterCreate) + assert.Nil(t, instance.DeletedAt) + assert.NoError(t, err) + }) + } } func TestUpdateNameInstance(t *testing.T) { - instanceRepo := repository.InstanceRepository(pool) - instanceId := gofakeit.Name() - instanceName := gofakeit.Name() + tests := []struct { + name string + testFunc func() *domain.Instance + rowsAffected int64 + }{ + { + name: "happy path", + testFunc: func() *domain.Instance { + instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() - ctx := context.Background() - inst := domain.Instance{ - ID: instanceId, - Name: instanceName, - DefaultOrgID: "defaultOrgId", - IAMProjectID: "iamProject", - ConsoleClientId: "consoleCLient", - ConsoleAppID: "consoleApp", - DefaultLanguage: "defaultLanguage", + ctx := context.Background() + inst := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + + // create instance + err := instanceRepo.Create(ctx, &inst) + assert.NoError(t, err) + return &inst + }, + rowsAffected: 1, + }, + { + name: "update non existent instance", + testFunc: func() *domain.Instance { + instanceId := gofakeit.Name() + + inst := domain.Instance{ + ID: instanceId, + } + return &inst + }, + rowsAffected: 0, + }, } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + beforeUpdate := time.Now() - err := instanceRepo.Create(ctx, &inst) - require.NoError(t, err) + ctx := context.Background() + instanceRepo := repository.InstanceRepository(pool) - _, err = instanceRepo.Get(ctx, - instanceRepo.NameCondition(database.TextOperationEqual, instanceName), - ) - require.NoError(t, err) + instance := tt.testFunc() - // update name - err = instanceRepo.Update(ctx, - instanceRepo.IDCondition(instanceId), - instanceRepo.SetName("new_name"), - ) - require.NoError(t, err) + // update name + newName := "new_" + instance.Name + rowsAffected, err := instanceRepo.Update(ctx, + instanceRepo.IDCondition(instance.ID), + instanceRepo.SetName(newName), + ) + afterUpdate := time.Now() + assert.NoError(t, err) - instance, err := instanceRepo.Get(ctx, - instanceRepo.IDCondition(instanceId), - ) - require.NoError(t, err) - require.Equal(t, "new_name", instance.Name) + assert.Equal(t, tt.rowsAffected, rowsAffected) + + if rowsAffected == 0 { + return + } + + // check instance values + instance, err = instanceRepo.Get(ctx, + instanceRepo.IDCondition(instance.ID), + ) + assert.NoError(t, err) + + assert.Equal(t, newName, instance.Name) + assert.WithinRange(t, instance.UpdatedAt, beforeUpdate, afterUpdate) + assert.Nil(t, instance.DeletedAt) + }) + } } -func TestUpdeDeleteInstance(t *testing.T) { +func TestGetInstance(t *testing.T) { + tests := []struct { + name string + testFunc func() *domain.Instance + noInstanceReturned bool + }{ + { + name: "happy path", + testFunc: func() *domain.Instance { + instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() + + ctx := context.Background() + inst := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + + // create instance + err := instanceRepo.Create(ctx, &inst) + assert.NoError(t, err) + return &inst + }, + }, + { + name: "get non existent instance", + testFunc: func() *domain.Instance { + instanceId := gofakeit.Name() + + inst := domain.Instance{ + ID: instanceId, + } + return &inst + }, + noInstanceReturned: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + instanceRepo := repository.InstanceRepository(pool) + + var instance *domain.Instance + if tt.testFunc != nil { + instance = tt.testFunc() + } + + // check instance values + returnedInstance, err := instanceRepo.Get(ctx, + instanceRepo.IDCondition(instance.ID), + ) + assert.NoError(t, err) + if tt.noInstanceReturned { + assert.Nil(t, returnedInstance) + return + } + + assert.Equal(t, returnedInstance.ID, instance.ID) + assert.Equal(t, returnedInstance.Name, instance.Name) + assert.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID) + assert.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID) + assert.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID) + assert.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID) + assert.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage) + assert.NoError(t, err) + }) + } +} + +func TestListInstance(t *testing.T) { + tests := []struct { + name string + testFunc func() *domain.Instance + + noInstanceReturned bool + }{ + { + name: "happy path", + testFunc: func() *domain.Instance { + instanceRepo := repository.InstanceRepository(pool) + instanceId := gofakeit.Name() + instanceName := gofakeit.Name() + + ctx := context.Background() + inst := domain.Instance{ + ID: instanceId, + Name: instanceName, + DefaultOrgID: "defaultOrgId", + IAMProjectID: "iamProject", + ConsoleClientID: "consoleCLient", + ConsoleAppID: "consoleApp", + DefaultLanguage: "defaultLanguage", + } + + // create instance + err := instanceRepo.Create(ctx, &inst) + assert.NoError(t, err) + return &inst + }, + }, + { + name: "get non existent instance", + testFunc: func() *domain.Instance { + instanceId := gofakeit.Name() + + inst := domain.Instance{ + ID: instanceId, + } + return &inst + }, + noInstanceReturned: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + instanceRepo := repository.InstanceRepository(pool) + + var instance *domain.Instance + if tt.testFunc != nil { + instance = tt.testFunc() + } + + // check instance values + returnedInstance, err := instanceRepo.List(ctx, + instanceRepo.IDCondition(instance.ID), + ) + assert.NoError(t, err) + if tt.noInstanceReturned { + assert.Nil(t, returnedInstance) + return + } + + assert.Equal(t, returnedInstance.ID, instance.ID) + assert.Equal(t, returnedInstance.Name, instance.Name) + assert.Equal(t, returnedInstance.DefaultOrgID, instance.DefaultOrgID) + assert.Equal(t, returnedInstance.IAMProjectID, instance.IAMProjectID) + assert.Equal(t, returnedInstance.ConsoleClientID, instance.ConsoleClientID) + assert.Equal(t, returnedInstance.ConsoleAppID, instance.ConsoleAppID) + assert.Equal(t, returnedInstance.DefaultLanguage, instance.DefaultLanguage) + assert.NoError(t, err) + }) + } +} + +func TestUpdateDeleteInstance(t *testing.T) { instanceRepo := repository.InstanceRepository(pool) instanceId := gofakeit.Name() instanceName := gofakeit.Name() @@ -100,29 +382,29 @@ func TestUpdeDeleteInstance(t *testing.T) { Name: instanceName, DefaultOrgID: "defaultOrgId", IAMProjectID: "iamProject", - ConsoleClientId: "consoleCLient", + ConsoleClientID: "consoleCLient", ConsoleAppID: "consoleApp", DefaultLanguage: "defaultLanguage", } err := instanceRepo.Create(ctx, &inst) - require.NoError(t, err) + assert.NoError(t, err) instance, err := instanceRepo.Get(ctx, instanceRepo.NameCondition(database.TextOperationEqual, instanceName), ) - require.NotNil(t, instance) - require.NoError(t, err) + assert.NotNil(t, instance) + assert.NoError(t, err) // delete instance err = instanceRepo.Delete(ctx, instanceRepo.IDCondition(instanceId), ) - require.NoError(t, err) + assert.NoError(t, err) instance, err = instanceRepo.Get(ctx, instanceRepo.NameCondition(database.TextOperationEqual, instanceName), ) - require.NoError(t, err) - require.Nil(t, instance) + assert.NoError(t, err) + assert.Nil(t, instance) } diff --git a/backend/v3/storage/database/repository/repository.go b/backend/v3/storage/database/repository/repository.go index 55b3edd7e6..ebd99a66d6 100644 --- a/backend/v3/storage/database/repository/repository.go +++ b/backend/v3/storage/database/repository/repository.go @@ -3,7 +3,6 @@ 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/user.go b/backend/v3/storage/database/repository/user.go index 872650b9b5..768279319b 100644 --- a/backend/v3/storage/database/repository/user.go +++ b/backend/v3/storage/database/repository/user.go @@ -120,7 +120,8 @@ func (u *user) Create(ctx context.Context, user *domain.User) error { func (u *user) Delete(ctx context.Context, condition database.Condition) error { u.builder.WriteString("DELETE FROM users") u.writeCondition(condition) - return u.client.Exec(ctx, u.builder.String(), u.builder.Args()...) + _, err := u.client.Exec(ctx, u.builder.String(), u.builder.Args()...) + return err } // ------------------------------------------------------------- diff --git a/backend/v3/storage/database/repository/user_human.go b/backend/v3/storage/database/repository/user_human.go index 123a834cb5..e0563c8be4 100644 --- a/backend/v3/storage/database/repository/user_human.go +++ b/backend/v3/storage/database/repository/user_human.go @@ -46,7 +46,8 @@ func (h userHuman) Update(ctx context.Context, condition database.Condition, cha stmt := h.builder.String() - return h.client.Exec(ctx, stmt, h.builder.Args()...) + _, err := h.client.Exec(ctx, stmt, h.builder.Args()...) + return err } // ------------------------------------------------------------- diff --git a/backend/v3/storage/database/repository/user_machine.go b/backend/v3/storage/database/repository/user_machine.go index f60001927b..ef4573838c 100644 --- a/backend/v3/storage/database/repository/user_machine.go +++ b/backend/v3/storage/database/repository/user_machine.go @@ -18,13 +18,14 @@ var _ domain.MachineRepository = (*userMachine)(nil) // ------------------------------------------------------------- // Update implements [domain.MachineRepository]. -func (m userMachine) Update(ctx context.Context, condition database.Condition, changes ...database.Change) (err error) { +func (m userMachine) Update(ctx context.Context, condition database.Condition, changes ...database.Change) error { m.builder.WriteString("UPDATE user_machines SET ") database.Changes(changes).Write(&m.builder) m.writeCondition(condition) m.writeReturning() - return m.client.Exec(ctx, m.builder.String(), m.builder.Args()...) + _, err := m.client.Exec(ctx, m.builder.String(), m.builder.Args()...) + return err } // ------------------------------------------------------------- diff --git a/backend/v3/storage/eventstore/event.go b/backend/v3/storage/eventstore/event.go index 1306a9329e..4a52bc860c 100644 --- a/backend/v3/storage/eventstore/event.go +++ b/backend/v3/storage/eventstore/event.go @@ -15,7 +15,8 @@ type Event struct { func Publish(ctx context.Context, events []*Event, db database.Executor) error { for _, event := range events { - if err := db.Exec(ctx, `INSERT INTO events (aggregate_type, aggregate_id) VALUES ($1, $2)`, event.AggregateType, event.AggregateID); err != nil { + _, err := db.Exec(ctx, `INSERT INTO events (aggregate_type, aggregate_id) VALUES ($1, $2)`, event.AggregateType, event.AggregateID) + if err != nil { return err } } diff --git a/go.mod b/go.mod index de0e8aa314..b82fce9863 100644 --- a/go.mod +++ b/go.mod @@ -43,6 +43,7 @@ require ( github.com/h2non/gock v1.2.0 github.com/hashicorp/golang-lru/v2 v2.0.7 github.com/improbable-eng/grpc-web v0.15.0 + github.com/jackc/pgconn v1.14.3 github.com/jackc/pgx/v5 v5.7.3 github.com/jackc/tern/v2 v2.3.3 github.com/jarcoal/jpath v0.0.0-20140328210829-f76b8b2dbf52 @@ -135,6 +136,9 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/huandu/xstrings v1.5.0 // indirect + github.com/jackc/chunkreader/v2 v2.0.1 // indirect + github.com/jackc/pgio v1.0.0 // indirect + github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/klauspost/cpuid/v2 v2.2.9 // indirect github.com/lib/pq v1.10.9 // indirect diff --git a/go.sum b/go.sum index ff10718898..b2313dec22 100644 --- a/go.sum +++ b/go.sum @@ -449,10 +449,19 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= +github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= +github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= +github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa h1:s+4MhCQ6YrzisK6hFJUX53drDT4UsSW3DEhKn0ifuHw= github.com/jackc/pgerrcode v0.0.0-20220416144525-469b46aa5efa/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= +github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= +github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= +github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgx/v5 v5.7.3 h1:PO1wNKj/bTAwxSJnO1Z4Ai8j4magtqg2SLNjEDzcXQo=