diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 093f7910..df468802 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -21,7 +21,7 @@ jobs: go-version: 'stable' cache-dependency-path: "**/go.sum" - name: Benchmark - run: go test -v -bench=. ./... + run: go test -v -bench=. -run=^$ ./... working-directory: ${{ matrix.module }} all: if: ${{ always() }} diff --git a/provider/appconfig/appconfig_test.go b/provider/appconfig/appconfig_test.go index 63afc9b8..b81eeab6 100644 --- a/provider/appconfig/appconfig_test.go +++ b/provider/appconfig/appconfig_test.go @@ -2,3 +2,194 @@ // Use of this source code is governed by a MIT license found in the LICENSE file. package appconfig //nolint:testpackage +import ( + "context" + "encoding/json" + "errors" + "sync" + "testing" + "time" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/appconfigdata" + + "github.com/nil-go/konf/provider/appconfig/internal/assert" +) + +func TestAppConfig_Load(t *testing.T) { + t.Parallel() + + testcases := []struct { + description string + client appConfigClient + unmarshal func([]byte, any) error + expected map[string]any + err string + }{ + { + description: "appconfig", + client: fakeAppConfigClient{ + getLatestConfiguration: func( + context.Context, + *appconfigdata.GetLatestConfigurationInput, + ...func(*appconfigdata.Options), + ) (*appconfigdata.GetLatestConfigurationOutput, error) { + return &appconfigdata.GetLatestConfigurationOutput{ + Configuration: []byte(`{"k":"v"}`), + NextPollConfigurationToken: aws.String("next-token"), + }, nil + }, + }, + unmarshal: json.Unmarshal, + expected: map[string]any{ + "k": "v", + }, + }, + { + description: "start session error", + client: fakeAppConfigClient{ + startConfigurationSession: func( + context.Context, + *appconfigdata.StartConfigurationSessionInput, + ...func(*appconfigdata.Options), + ) (*appconfigdata.StartConfigurationSessionOutput, error) { + return nil, errors.New("start session error") + }, + }, + unmarshal: json.Unmarshal, + err: "start configuration session: start session error", + }, + { + description: "get configuration error", + client: fakeAppConfigClient{ + getLatestConfiguration: func( + context.Context, + *appconfigdata.GetLatestConfigurationInput, + ...func(*appconfigdata.Options), + ) (*appconfigdata.GetLatestConfigurationOutput, error) { + return nil, errors.New("get configuration error") + }, + }, + err: "get latest configuration: get configuration error", + }, + { + description: "unmarshal error", + client: fakeAppConfigClient{}, + unmarshal: func([]byte, any) error { + return errors.New("unmarshal error") + }, + err: "unmarshal: unmarshal error", + }, + } + + for i := range testcases { + testcase := testcases[i] + + t.Run(testcase.description, func(t *testing.T) { + t.Parallel() + + values, err := (&AppConfig{client: testcase.client, unmarshal: testcase.unmarshal}).Load() + if testcase.err != "" { + assert.EqualError(t, err, testcase.err) + } else { + assert.NoError(t, err) + assert.Equal(t, testcase.expected, values) + } + }) + } +} + +func TestAppConfig_Watch(t *testing.T) { + testcases := []struct { + description string + client appConfigClient + expected map[string]any + }{ + { + description: "get latest configuration", + client: fakeAppConfigClient{ + getLatestConfiguration: func( + context.Context, + *appconfigdata.GetLatestConfigurationInput, + ...func(*appconfigdata.Options), + ) (*appconfigdata.GetLatestConfigurationOutput, error) { + return &appconfigdata.GetLatestConfigurationOutput{ + Configuration: []byte(`{"k":"v"}`), + NextPollConfigurationToken: aws.String("next-token"), + }, nil + }, + }, + expected: map[string]any{"k": "v"}, + }, + } + + for i := range testcases { + testcase := testcases[i] + + t.Run(testcase.description, func(t *testing.T) { + loader := &AppConfig{client: testcase.client, unmarshal: json.Unmarshal, pollInterval: time.Second} + var values map[string]any + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + var waitGroup sync.WaitGroup + waitGroup.Add(1) + go func() { + err := loader.Watch(ctx, func(changed map[string]any) { + defer waitGroup.Done() + values = changed + }) + assert.NoError(t, err) + }() + + waitGroup.Wait() + assert.Equal(t, testcase.expected, values) + }) + } +} + +type fakeAppConfigClient struct { + startConfigurationSession func( + ctx context.Context, + params *appconfigdata.StartConfigurationSessionInput, + optFns ...func(*appconfigdata.Options), + ) (*appconfigdata.StartConfigurationSessionOutput, error) + getLatestConfiguration func( + ctx context.Context, + params *appconfigdata.GetLatestConfigurationInput, + optFns ...func(*appconfigdata.Options), + ) (*appconfigdata.GetLatestConfigurationOutput, error) +} + +func (f fakeAppConfigClient) StartConfigurationSession( + ctx context.Context, + params *appconfigdata.StartConfigurationSessionInput, + optFns ...func(*appconfigdata.Options), +) (*appconfigdata.StartConfigurationSessionOutput, error) { + if f.startConfigurationSession != nil { + return f.startConfigurationSession(ctx, params, optFns...) + } + + return &appconfigdata.StartConfigurationSessionOutput{InitialConfigurationToken: aws.String("initial-token")}, nil +} + +func (f fakeAppConfigClient) GetLatestConfiguration( + ctx context.Context, + params *appconfigdata.GetLatestConfigurationInput, + optFns ...func(*appconfigdata.Options), +) (*appconfigdata.GetLatestConfigurationOutput, error) { + if f.getLatestConfiguration != nil { + return f.getLatestConfiguration(ctx, params, optFns...) + } + + return &appconfigdata.GetLatestConfigurationOutput{ + NextPollConfigurationToken: aws.String("next-token"), + Configuration: make([]byte, 0), + }, nil +} + +func TestAppConfig_String(t *testing.T) { + t.Parallel() + + assert.Equal(t, "appConfig:app-env-profile", New("app", "env", "profile").String()) +} diff --git a/provider/appconfig/benchmark_test.go b/provider/appconfig/benchmark_test.go index 63afc9b8..d09b8631 100644 --- a/provider/appconfig/benchmark_test.go +++ b/provider/appconfig/benchmark_test.go @@ -2,3 +2,54 @@ // Use of this source code is governed by a MIT license found in the LICENSE file. package appconfig //nolint:testpackage +import ( + "context" + "encoding/json" + "testing" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/appconfigdata" + + "github.com/nil-go/konf/provider/appconfig/internal/assert" +) + +func BenchmarkNew(b *testing.B) { + var loader *AppConfig + for i := 0; i < b.N; i++ { + loader = New("app", "env", "profile") + } + b.StopTimer() + + assert.Equal(b, "appConfig:app-env-profile", loader.String()) +} + +func BenchmarkLoad(b *testing.B) { + loader := &AppConfig{ + client: fakeAppConfigClient{ + getLatestConfiguration: func( + context.Context, + *appconfigdata.GetLatestConfigurationInput, + ...func(*appconfigdata.Options), + ) (*appconfigdata.GetLatestConfigurationOutput, error) { + return &appconfigdata.GetLatestConfigurationOutput{ + Configuration: []byte(`{"k":"v"}`), + NextPollConfigurationToken: aws.String("next-token"), + }, nil + }, + }, + unmarshal: json.Unmarshal, + } + b.ResetTimer() + + var ( + values map[string]any + err error + ) + for i := 0; i < b.N; i++ { + values, err = loader.Load() + } + b.StopTimer() + + assert.NoError(b, err) + assert.Equal(b, "v", values["k"]) +} diff --git a/provider/appconfig/internal/assert/assert.go b/provider/appconfig/internal/assert/assert.go new file mode 100644 index 00000000..8b030a3b --- /dev/null +++ b/provider/appconfig/internal/assert/assert.go @@ -0,0 +1,36 @@ +// Copyright (c) 2024 The konf authors +// Use of this source code is governed by a MIT license found in the LICENSE file. + +package assert + +import ( + "reflect" + "testing" +) + +func Equal[T any](tb testing.TB, expected, actual T) { + tb.Helper() + + if !reflect.DeepEqual(expected, actual) { + tb.Errorf("expected: %v; actual: %v", expected, actual) + } +} + +func NoError(tb testing.TB, err error) { + tb.Helper() + + if err != nil { + tb.Errorf("unexpected error: %v", err) + } +} + +func EqualError(tb testing.TB, err error, message string) { + tb.Helper() + + switch { + case err == nil: + tb.Errorf("expected: %v; actual: ", message) + case err.Error() != message: + tb.Errorf("expected: %v; actual: %v", message, err.Error()) + } +} diff --git a/provider/file/file_test.go b/provider/file/file_test.go index 45220f89..565c7461 100644 --- a/provider/file/file_test.go +++ b/provider/file/file_test.go @@ -7,7 +7,6 @@ package file_test import ( "errors" - "strings" "testing" "github.com/nil-go/konf/provider/file" @@ -34,7 +33,7 @@ func TestFile_Load(t *testing.T) { { description: "file (not exist)", path: "not_found.json", - err: "read file: open not_found.json: ", + err: "read file: open not_found.json: no such file or directory", }, { description: "unmarshal error", @@ -56,7 +55,7 @@ func TestFile_Load(t *testing.T) { values, err := file.New(testcase.path, testcase.opts...).Load() if testcase.err != "" { - assert.True(t, strings.HasPrefix(err.Error(), testcase.err)) + assert.EqualError(t, err, testcase.err) } else { assert.NoError(t, err) assert.Equal(t, testcase.expected, values) diff --git a/provider/file/internal/assert/assert.go b/provider/file/internal/assert/assert.go index ba142ed2..8b030a3b 100644 --- a/provider/file/internal/assert/assert.go +++ b/provider/file/internal/assert/assert.go @@ -24,10 +24,13 @@ func NoError(tb testing.TB, err error) { } } -func True(tb testing.TB, value bool) { +func EqualError(tb testing.TB, err error, message string) { tb.Helper() - if !value { - tb.Errorf("expected True") + switch { + case err == nil: + tb.Errorf("expected: %v; actual: ", message) + case err.Error() != message: + tb.Errorf("expected: %v; actual: %v", message, err.Error()) } }