Skip to content

Commit

Permalink
Module tests can declare module context values
Browse files Browse the repository at this point in the history
  • Loading branch information
matt2e committed Apr 22, 2024
1 parent 56221ef commit 8f6de76
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 12 deletions.
51 changes: 51 additions & 0 deletions go-runtime/ftl/ftltest/contextbuilder.go
Original file line number Diff line number Diff line change
@@ -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
}
56 changes: 44 additions & 12 deletions go-runtime/modulecontext/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
43 changes: 43 additions & 0 deletions go-runtime/modulecontext/builder_test.go
Original file line number Diff line number Diff line change
@@ -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")
}

0 comments on commit 8f6de76

Please sign in to comment.