package ldfiledata import ( "errors" "os" "testing" "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks" "github.com/launchdarkly/go-sdk-common/v3/ldlog" "github.com/launchdarkly/go-sdk-common/v3/ldlogtest" "github.com/launchdarkly/go-sdk-common/v3/ldvalue" "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel" "github.com/launchdarkly/go-server-sdk/v6/interfaces" "github.com/launchdarkly/go-server-sdk/v6/internal/datakinds" "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest" "github.com/launchdarkly/go-server-sdk/v6/ldcomponents" "github.com/launchdarkly/go-server-sdk/v6/subsystems" th "github.com/launchdarkly/go-test-helpers/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type fileDataSourceTestParams struct { dataSource subsystems.DataSource updates *mocks.MockDataSourceUpdates mockLog *ldlogtest.MockLog closeWhenReady chan struct{} } func (p fileDataSourceTestParams) waitForStart() { p.dataSource.Start(p.closeWhenReady) <-p.closeWhenReady } func withFileDataSourceTestParams( factory subsystems.ComponentConfigurer[subsystems.DataSource], action func(fileDataSourceTestParams), ) { p := fileDataSourceTestParams{} mockLog := ldlogtest.NewMockLog() testContext := sharedtest.NewTestContext("", nil, &subsystems.LoggingConfiguration{Loggers: mockLog.Loggers}) store, _ := ldcomponents.InMemoryDataStore().Build(testContext) updates := mocks.NewMockDataSourceUpdates(store) testContext.DataSourceUpdateSink = updates dataSource, err := factory.Build(testContext) if err != nil { panic(err) } defer dataSource.Close() p.dataSource = dataSource action(fileDataSourceTestParams{dataSource, updates, mockLog, make(chan struct{})}) } func expectCreationError(t *testing.T, factory subsystems.ComponentConfigurer[subsystems.DataSource]) error { testContext := sharedtest.NewTestContext("", nil, nil) store, _ := ldcomponents.InMemoryDataStore().Build(testContext) updates := mocks.NewMockDataSourceUpdates(store) testContext.DataSourceUpdateSink = updates dataSource, err := factory.Build(testContext) require.Error(t, err) require.Nil(t, dataSource) return err } func TestNewFileDataSourceYaml(t *testing.T) { fileData := ` --- flags: my-flag: "on": true segments: my-segment: rules: [] ` th.WithTempFileData([]byte(fileData), func(filename string) { factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.True(t, p.dataSource.IsInitialized()) flag := requireFlag(t, p.updates.DataStore, "my-flag") assert.True(t, flag.On) segment := requireSegment(t, p.updates.DataStore, "my-segment") assert.Empty(t, segment.Rules) }) }) } func TestNewFileDataSourceJson(t *testing.T) { th.WithTempFileData([]byte(`{"flags": {"my-flag": {"on": true}}}`), func(filename string) { factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.True(t, p.dataSource.IsInitialized()) flag := requireFlag(t, p.updates.DataStore, "my-flag") assert.True(t, flag.On) }) }) } func TestStatusIsValidAfterSuccessfulLoad(t *testing.T) { th.WithTempFileData([]byte(`{"flags": {"my-flag": {"on": true}}}`), func(filename string) { factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.True(t, p.dataSource.IsInitialized()) p.updates.RequireStatusOf(t, interfaces.DataSourceStateValid) }) }) } func TestNewFileDataSourceJsonWithTwoFiles(t *testing.T) { th.WithTempFileData([]byte(`{"flags": {"my-flag1": {"on": true}}}`), func(filename1 string) { th.WithTempFileData([]byte(`{"flags": {"my-flag2": {"on": true}}}`), func(filename2 string) { factory := DataSource().FilePaths(filename1, filename2) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.True(t, p.dataSource.IsInitialized()) flag1 := requireFlag(t, p.updates.DataStore, "my-flag1") assert.True(t, flag1.On) flag2 := requireFlag(t, p.updates.DataStore, "my-flag2") assert.True(t, flag2.On) }) }) }) } func TestNewFileDataSourceJsonWithTwoConflictingFiles(t *testing.T) { file1Data := `{"flags": {"flag1": {"on": true}, "flag2": {"on": true}}, "segments": {"segment1": {}}}` file2Data := `{"flags": {"flag2": {"on": true}}}` file3Data := `{"flagValues": {"flag2": true}}` file4Data := `{"segments": {"segment1": {}}}` th.WithTempFileData([]byte(file1Data), func(filename1 string) { for _, data := range []string{file2Data, file3Data, file4Data} { th.WithTempFileData([]byte(data), func(filename2 string) { factory := DataSource().FilePaths(filename1, filename2) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.False(t, p.dataSource.IsInitialized()) p.mockLog.AssertMessageMatch(t, true, ldlog.Error, "specified by multiple files") }) }) } }) } func TestDuplicateKeysHandlingCanSuppressErrors(t *testing.T) { file1Data := `{"flags": {"flag1": {"on": true}, "flag2": {"on": false}}, "segments": {"segment1": {}}}` file2Data := `{"flags": {"flag2": {"on": true}}}` th.WithTempFileData([]byte(file1Data), func(filename1 string) { th.WithTempFileData([]byte(file2Data), func(filename2 string) { factory := DataSource().FilePaths(filename1, filename2). DuplicateKeysHandling(DuplicateKeysIgnoreAllButFirst) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.True(t, p.dataSource.IsInitialized()) flag2 := requireFlag(t, p.updates.DataStore, "flag2") assert.False(t, flag2.On) p.mockLog.AssertMessageMatch(t, false, ldlog.Error, "specified by multiple files") }) }) }) } func TestNewFileDataSourceBadData(t *testing.T) { th.WithTempFileData([]byte(`bad data`), func(filename string) { factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.False(t, p.dataSource.IsInitialized()) }) }) } func TestNewFileDataSourceMissingFile(t *testing.T) { th.WithTempFileData([]byte{}, func(filename string) { os.Remove(filename) factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() assert.False(t, p.dataSource.IsInitialized()) }) }) } func TestStatusIsInterruptedAfterUnsuccessfulLoad(t *testing.T) { th.WithTempFileData([]byte(`bad data`), func(filename string) { factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.False(t, p.dataSource.IsInitialized()) p.updates.RequireStatusOf(t, interfaces.DataSourceStateInterrupted) }) }) } func TestNewFileDataSourceYamlValues(t *testing.T) { fileData := ` --- flagValues: my-flag: true ` th.WithTempFileData([]byte(fileData), func(filename string) { factory := DataSource().FilePaths(filename) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() require.True(t, p.dataSource.IsInitialized()) flag := requireFlag(t, p.updates.DataStore, "my-flag") assert.Equal(t, []ldvalue.Value{ldvalue.Bool(true)}, flag.Variations) }) }) } func TestReloaderFailureDoesNotPreventStarting(t *testing.T) { e := errors.New("sorry") f := func(paths []string, loggers ldlog.Loggers, reload func(), closeCh <-chan struct{}) error { return e } factory := DataSource().Reloader(f) withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) { p.waitForStart() assert.True(t, p.dataSource.IsInitialized()) assert.Len(t, p.mockLog.GetOutput(ldlog.Error), 1) }) } func requireFlag(t *testing.T, store subsystems.DataStore, key string) *ldmodel.FeatureFlag { item, err := store.Get(datakinds.Features, key) require.NoError(t, err) require.NotNil(t, item.Item) return item.Item.(*ldmodel.FeatureFlag) } func requireSegment(t *testing.T, store subsystems.DataStore, key string) *ldmodel.Segment { item, err := store.Get(datakinds.Segments, key) require.NoError(t, err) require.NotNil(t, item.Item) return item.Item.(*ldmodel.Segment) }