feat: add embedded testing server for postgres (#9955)

# Which Problems Are Solved

1. there was no embedded database to run tests against
2. there were no tests for postgres/migrate
3. there was no test setup for repository which starts a client for the
embedded database

# How the Problems Are Solved

1. postgres/embedded package was added
2. tests were added
3. TestMain was added incl. an example test

# Additional Changes

none

# Additional Context

closes #9934

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Silvan
2025-05-26 09:31:45 +02:00
committed by GitHub
parent 362420f62b
commit 01180d2a63
11 changed files with 274 additions and 19 deletions

View File

@@ -8,6 +8,7 @@ import (
type Pool interface {
Beginner
QueryExecutor
Migrator
Acquire(ctx context.Context) (Client, error)
Close(ctx context.Context) error
@@ -17,6 +18,7 @@ type Pool interface {
type Client interface {
Beginner
QueryExecutor
Migrator
Release(ctx context.Context) error
}

View File

@@ -199,6 +199,44 @@ func (c *MockPoolExecCall) DoAndReturn(f func(context.Context, string, ...any) e
return c
}
// Migrate mocks base method.
func (m *MockPool) Migrate(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Migrate", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Migrate indicates an expected call of Migrate.
func (mr *MockPoolMockRecorder) Migrate(arg0 any) *MockPoolMigrateCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockPool)(nil).Migrate), arg0)
return &MockPoolMigrateCall{Call: call}
}
// MockPoolMigrateCall wrap *gomock.Call
type MockPoolMigrateCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockPoolMigrateCall) Return(arg0 error) *MockPoolMigrateCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockPoolMigrateCall) Do(f func(context.Context) error) *MockPoolMigrateCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockPoolMigrateCall) DoAndReturn(f func(context.Context) error) *MockPoolMigrateCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Query mocks base method.
func (m *MockPool) Query(arg0 context.Context, arg1 string, arg2 ...any) (database.Rows, error) {
m.ctrl.T.Helper()
@@ -391,6 +429,44 @@ func (c *MockClientExecCall) DoAndReturn(f func(context.Context, string, ...any)
return c
}
// Migrate mocks base method.
func (m *MockClient) Migrate(arg0 context.Context) error {
m.ctrl.T.Helper()
ret := m.ctrl.Call(m, "Migrate", arg0)
ret0, _ := ret[0].(error)
return ret0
}
// Migrate indicates an expected call of Migrate.
func (mr *MockClientMockRecorder) Migrate(arg0 any) *MockClientMigrateCall {
mr.mock.ctrl.T.Helper()
call := mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Migrate", reflect.TypeOf((*MockClient)(nil).Migrate), arg0)
return &MockClientMigrateCall{Call: call}
}
// MockClientMigrateCall wrap *gomock.Call
type MockClientMigrateCall struct {
*gomock.Call
}
// Return rewrite *gomock.Call.Return
func (c *MockClientMigrateCall) Return(arg0 error) *MockClientMigrateCall {
c.Call = c.Call.Return(arg0)
return c
}
// Do rewrite *gomock.Call.Do
func (c *MockClientMigrateCall) Do(f func(context.Context) error) *MockClientMigrateCall {
c.Call = c.Call.Do(f)
return c
}
// DoAndReturn rewrite *gomock.Call.DoAndReturn
func (c *MockClientMigrateCall) DoAndReturn(f func(context.Context) error) *MockClientMigrateCall {
c.Call = c.Call.DoAndReturn(f)
return c
}
// Query mocks base method.
func (m *MockClient) Query(arg0 context.Context, arg1 string, arg2 ...any) (database.Rows, error) {
m.ctrl.T.Helper()

View File

@@ -13,8 +13,9 @@ import (
)
var (
_ database.Connector = (*Config)(nil)
Name = "postgres"
_ database.Connector = (*Config)(nil)
Name = "postgres"
isMigrated bool
)
type Config struct {
@@ -45,7 +46,7 @@ func (c *Config) Connect(ctx context.Context) (database.Pool, error) {
if err = pool.Ping(ctx); err != nil {
return nil, err
}
return &pgxPool{pool}, nil
return &pgxPool{Pool: pool}, nil
}
func (c *Config) getPool(ctx context.Context) (*pgxpool.Pool, error) {

View File

@@ -9,11 +9,12 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres/migration"
)
type pgxConn struct{ *pgxpool.Conn }
type pgxConn struct {
*pgxpool.Conn
}
var (
_ database.Client = (*pgxConn)(nil)
_ database.Migrator = (*pgxConn)(nil)
_ database.Client = (*pgxConn)(nil)
)
// Release implements [database.Client].
@@ -53,5 +54,10 @@ func (c *pgxConn) Exec(ctx context.Context, sql string, args ...any) error {
// Migrate implements [database.Migrator].
func (c *pgxConn) Migrate(ctx context.Context) error {
return migration.Migrate(ctx, c.Conn.Conn())
if isMigrated {
return nil
}
err := migration.Migrate(ctx, c.Conn.Conn())
isMigrated = err == nil
return err
}

View File

@@ -0,0 +1,50 @@
// embedded is used for testing purposes
package embedded
import (
"net"
"os"
embeddedpostgres "github.com/fergusstrange/embedded-postgres"
"github.com/zitadel/logging"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres"
)
// StartEmbedded starts an embedded postgres v16 instance and returns a database connector and a stop function
// the database is started on a random port and data are stored in a temporary directory
// its used for testing purposes only
func StartEmbedded() (connector database.Connector, stop func(), err error) {
path, err := os.MkdirTemp("", "zitadel-embedded-postgres-*")
logging.OnError(err).Fatal("unable to create temp dir")
port, close := getPort()
config := embeddedpostgres.DefaultConfig().Version(embeddedpostgres.V16).Port(uint32(port)).RuntimePath(path)
embedded := embeddedpostgres.NewDatabase(config)
close()
err = embedded.Start()
logging.OnError(err).Fatal("unable to start db")
connector, err = postgres.DecodeConfig(config.GetConnectionURL())
if err != nil {
return nil, nil, err
}
return connector, func() {
logging.OnError(embedded.Stop()).Error("unable to stop db")
}, nil
}
// getPort returns a free port and locks it until close is called
func getPort() (port uint16, close func()) {
l, err := net.Listen("tcp", ":0")
logging.OnError(err).Fatal("unable to get port")
port = uint16(l.Addr().(*net.TCPAddr).Port)
logging.WithFields("port", port).Info("Port is available")
return port, func() {
logging.OnError(l.Close()).Error("unable to close port listener")
}
}

View File

@@ -0,0 +1,60 @@
package migration_test
import (
"context"
"testing"
"github.com/muhlemmer/gu"
"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/embedded"
)
func TestMigrate(t *testing.T) {
tests := []struct {
name string
stmt string
args []any
res []any
}{
{
name: "schema",
stmt: "SELECT EXISTS(SELECT 1 FROM information_schema.schemata where schema_name = 'zitadel') ;",
res: []any{true},
},
{
name: "001",
stmt: "SELECT EXISTS(SELECT 1 FROM pg_catalog.pg_tables WHERE schemaname = 'zitadel' and tablename=$1)",
args: []any{"instances"},
res: []any{true},
},
}
ctx := context.Background()
connector, stop, err := embedded.StartEmbedded()
require.NoError(t, err, "failed to start embedded postgres")
defer stop()
client, err := connector.Connect(ctx)
require.NoError(t, err, "failed to connect to embedded postgres")
err = client.(database.Migrator).Migrate(ctx)
require.NoError(t, err, "failed to execute migration steps")
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := make([]any, len(tt.res))
for i := range got {
got[i] = new(any)
tt.res[i] = gu.Ptr(tt.res[i])
}
require.NoError(t, client.QueryRow(ctx, tt.stmt, tt.args...).Scan(got...), "failed to execute check query")
assert.Equal(t, tt.res, got, "query result does not match")
})
}
}

View File

@@ -9,11 +9,12 @@ import (
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres/migration"
)
type pgxPool struct{ *pgxpool.Pool }
type pgxPool struct {
*pgxpool.Pool
}
var (
_ database.Pool = (*pgxPool)(nil)
_ database.Migrator = (*pgxPool)(nil)
_ database.Pool = (*pgxPool)(nil)
)
// Acquire implements [database.Pool].
@@ -22,7 +23,7 @@ func (c *pgxPool) Acquire(ctx context.Context) (database.Client, error) {
if err != nil {
return nil, err
}
return &pgxConn{conn}, nil
return &pgxConn{Conn: conn}, nil
}
// Query implements [database.Pool].
@@ -62,9 +63,16 @@ func (c *pgxPool) Close(_ context.Context) error {
// Migrate implements [database.Migrator].
func (c *pgxPool) Migrate(ctx context.Context) error {
if isMigrated {
return nil
}
client, err := c.Pool.Acquire(ctx)
if err != nil {
return err
}
return migration.Migrate(ctx, client.Conn())
err = migration.Migrate(ctx, client.Conn())
isMigrated = err == nil
return err
}

View File

@@ -4,5 +4,6 @@ import "context"
type Migrator interface {
// Migrate executes migrations to setup the database.
// The method can be called once per running Zitadel.
Migrate(ctx context.Context) error
}

View File

@@ -0,0 +1,16 @@
package repository
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
)
// TestBla is an example and can be removed later
func TestBla(t *testing.T) {
var count int
err := pool.QueryRow(context.Background(), "select count(*) from zitadel.instances").Scan(&count)
assert.NoError(t, err)
assert.Equal(t, 0, count)
}

View File

@@ -0,0 +1,41 @@
package repository
import (
"context"
"log"
"os"
"testing"
"github.com/zitadel/zitadel/backend/v3/storage/database"
"github.com/zitadel/zitadel/backend/v3/storage/database/dialect/postgres/embedded"
)
func TestMain(m *testing.M) {
os.Exit(runTests(m))
}
var pool database.Pool
func runTests(m *testing.M) int {
connector, stop, err := embedded.StartEmbedded()
if err != nil {
log.Fatalf("unable to start embedded postgres: %v", err)
}
defer stop()
ctx := context.Background()
pool, err = connector.Connect(ctx)
if err != nil {
log.Printf("unable to connect to embedded postgres: %v", err)
return 1
}
err = pool.Migrate(ctx)
if err != nil {
log.Printf("unable to migrate database: %v", err)
return 1
}
return m.Run()
}