From 8f6de762000cce24bff197c11df5ebc07891856f Mon Sep 17 00:00:00 2001 From: Matt Toohey Date: Mon, 22 Apr 2024 15:39:35 +1000 Subject: [PATCH] Module tests can declare module context values --- go-runtime/ftl/ftltest/contextbuilder.go | 51 +++++++++++++++++++++ go-runtime/modulecontext/builder.go | 56 +++++++++++++++++++----- go-runtime/modulecontext/builder_test.go | 43 ++++++++++++++++++ 3 files changed, 138 insertions(+), 12 deletions(-) create mode 100644 go-runtime/ftl/ftltest/contextbuilder.go create mode 100644 go-runtime/modulecontext/builder_test.go diff --git a/go-runtime/ftl/ftltest/contextbuilder.go b/go-runtime/ftl/ftltest/contextbuilder.go new file mode 100644 index 0000000000..5158ea0479 --- /dev/null +++ b/go-runtime/ftl/ftltest/contextbuilder.go @@ -0,0 +1,51 @@ +package ftltest + +import ( + "context" + + "github.com/TBD54566975/ftl/go-runtime/modulecontext" +) + +type DBType int32 + +const ( + DBTypePostgres = DBType(modulecontext.DBTypePostgres) +) + +// ContextBuilder allows for building a context with configuration, secrets and DSN values set up for tests +type ContextBuilder struct { + builder *modulecontext.Builder +} + +func NewContextBuilder(moduleName string) *ContextBuilder { + return &ContextBuilder{ + builder: modulecontext.NewBuilder(moduleName), + } +} + +func (b *ContextBuilder) AddConfig(name string, value any) *ContextBuilder { + b.builder.AddConfig(name, value) + return b +} + +func (b *ContextBuilder) AddSecret(name string, value any) *ContextBuilder { + b.builder.AddSecret(name, value) + return b +} + +func (b *ContextBuilder) AddDSN(name string, dbType DBType, dsn string) *ContextBuilder { + b.builder.AddDSN(name, modulecontext.DBType(dbType), dsn) + return b +} + +func (b *ContextBuilder) Build() (context.Context, error) { + return b.BuildWithContext(Context()) +} + +func (b *ContextBuilder) BuildWithContext(ctx context.Context) (context.Context, error) { + moduleCtx, err := b.builder.Build(ctx) + if err != nil { + return nil, err + } + return moduleCtx.ApplyToContext(ctx), nil +} diff --git a/go-runtime/modulecontext/builder.go b/go-runtime/modulecontext/builder.go index e4ee42285f..745679e71a 100644 --- a/go-runtime/modulecontext/builder.go +++ b/go-runtime/modulecontext/builder.go @@ -2,6 +2,7 @@ package modulecontext import ( "context" + "fmt" ftlv1 "github.com/TBD54566975/ftl/backend/protos/xyz/block/ftl/v1" cf "github.com/TBD54566975/ftl/common/configuration" @@ -13,32 +14,37 @@ type dsnEntry struct { dsn string } +type valueOrData struct { + value optional.Option[any] + data optional.Option[[]byte] +} + // Builder is used to set up a ModuleContext with configs, secrets and DSNs // It is able to parse a ModuleContextResponse type Builder struct { moduleName string - configs map[string][]byte - secrets map[string][]byte + configs map[string]valueOrData + secrets map[string]valueOrData dsns map[string]dsnEntry } func NewBuilder(moduleName string) *Builder { return &Builder{ moduleName: moduleName, - configs: map[string][]byte{}, - secrets: map[string][]byte{}, + configs: map[string]valueOrData{}, + secrets: map[string]valueOrData{}, dsns: map[string]dsnEntry{}, } } func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextResponse) *Builder { - configs := map[string][]byte{} + configs := map[string]valueOrData{} for name, bytes := range response.Configs { - configs[name] = bytes + configs[name] = valueOrData{data: optional.Some(bytes)} } - secrets := map[string][]byte{} + secrets := map[string]valueOrData{} for name, bytes := range response.Secrets { - secrets[name] = bytes + secrets[name] = valueOrData{data: optional.Some(bytes)} } dsns := map[string]dsnEntry{} for _, d := range response.Databases { @@ -52,6 +58,24 @@ func NewBuilderFromProto(moduleName string, response *ftlv1.ModuleContextRespons } } +func (b *Builder) AddConfig(name string, value any) *Builder { + b.configs[name] = valueOrData{value: optional.Some(value)} + return b +} + +func (b *Builder) AddSecret(name string, value any) *Builder { + b.secrets[name] = valueOrData{value: optional.Some(value)} + return b +} + +func (b *Builder) AddDSN(name string, dbType DBType, dsn string) *Builder { + b.dsns[name] = dsnEntry{ + dbType: dbType, + dsn: dsn, + } + return b +} + func (b *Builder) Build(ctx context.Context) (*ModuleContext, error) { cm, err := newInMemoryConfigManager[cf.Configuration](ctx) if err != nil { @@ -91,10 +115,18 @@ func newInMemoryConfigManager[R cf.Role](ctx context.Context) (*cf.Manager[R], e return manager, nil } -func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], valueMap map[string][]byte, moduleName string) error { - for name, data := range valueMap { - if err := manager.SetData(ctx, cf.Ref{Module: optional.Some(moduleName), Name: name}, data); err != nil { - return err +func buildConfigOrSecrets[R cf.Role](ctx context.Context, manager cf.Manager[R], valueMap map[string]valueOrData, moduleName string) error { + for name, valueOrData := range valueMap { + if value, ok := valueOrData.value.Get(); ok { + if err := manager.Set(ctx, cf.Ref{Module: optional.Some(moduleName), Name: name}, value); err != nil { + return err + } + } else if data, ok := valueOrData.data.Get(); ok { + if err := manager.SetData(ctx, cf.Ref{Module: optional.Some(moduleName), Name: name}, data); err != nil { + return err + } + } else { + return fmt.Errorf("could not read value for name %q", name) } } return nil diff --git a/go-runtime/modulecontext/builder_test.go b/go-runtime/modulecontext/builder_test.go new file mode 100644 index 0000000000..675f281b97 --- /dev/null +++ b/go-runtime/modulecontext/builder_test.go @@ -0,0 +1,43 @@ +package modulecontext + +import ( + "context" + "testing" + + "github.com/TBD54566975/ftl/internal/log" + "github.com/alecthomas/assert/v2" + "github.com/alecthomas/types/optional" + + cf "github.com/TBD54566975/ftl/common/configuration" +) + +func TestReadLatestValue(t *testing.T) { + ctx := log.ContextWithNewDefaultLogger(context.Background()) + + moduleName := "test" + b := NewBuilder(moduleName) + b = b.AddConfig("c", "c-value1") + b = b.AddConfig("c", "c-value2") + b = b.AddSecret("s", "s-value1") + b = b.AddSecret("s", "s-value2") + b = b.AddDSN("d", DBTypePostgres, "postgres://localhost:54320/echo1?sslmode=disable&user=postgres&password=secret") + b = b.AddDSN("d", DBTypePostgres, "postgres://localhost:54320/echo2?sslmode=disable&user=postgres&password=secret") + moduleCtx, err := b.Build(ctx) + assert.NoError(t, err, "there should be no build errors") + ctx = moduleCtx.ApplyToContext(ctx) + + cm := cf.ConfigFromContext(ctx) + sm := cf.SecretsFromContext(ctx) + + var str string + + err = cm.Get(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "c"}, &str) + assert.NoError(t, err, "could not read config value") + assert.Equal(t, "c-value2", str, "latest config value should be read") + + err = sm.Get(ctx, cf.Ref{Module: optional.Some(moduleName), Name: "s"}, &str) + assert.NoError(t, err, "could not read secret value") + assert.Equal(t, "s-value2", str, "latest secret value should be read") + + assert.Equal(t, moduleCtx.dbProvider.entries["d"].dsn, "postgres://localhost:54320/echo2?sslmode=disable&user=postgres&password=secret", "latest DSN should be read") +}