...

Source file src/github.com/launchdarkly/go-server-sdk/v6/ldfilewatch/watched_file_data_source_test.go

Documentation: github.com/launchdarkly/go-server-sdk/v6/ldfilewatch

     1  package ldfilewatch
     2  
     3  import (
     4  	"os"
     5  	"path"
     6  	"testing"
     7  	"time"
     8  
     9  	"github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks"
    10  
    11  	"github.com/stretchr/testify/assert"
    12  	"github.com/stretchr/testify/require"
    13  
    14  	"github.com/launchdarkly/go-sdk-common/v3/ldlog"
    15  	"github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
    16  	"github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
    17  	"github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
    18  	"github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
    19  	"github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
    20  	"github.com/launchdarkly/go-server-sdk/v6/ldfiledata"
    21  	"github.com/launchdarkly/go-server-sdk/v6/subsystems"
    22  )
    23  
    24  type fileDataSourceTestParams struct {
    25  	dataSource     subsystems.DataSource
    26  	updates        *mocks.MockDataSourceUpdates
    27  	mockLog        *ldlogtest.MockLog
    28  	closeWhenReady chan struct{}
    29  }
    30  
    31  func (p fileDataSourceTestParams) waitForStart() {
    32  	p.dataSource.Start(p.closeWhenReady)
    33  	<-p.closeWhenReady
    34  }
    35  
    36  func withFileDataSourceTestParams(
    37  	factory subsystems.ComponentConfigurer[subsystems.DataSource],
    38  	action func(fileDataSourceTestParams),
    39  ) {
    40  	p := fileDataSourceTestParams{}
    41  	p.closeWhenReady = make(chan struct{})
    42  	p.mockLog = ldlogtest.NewMockLog()
    43  	testContext := sharedtest.NewTestContext("", nil, &subsystems.LoggingConfiguration{Loggers: p.mockLog.Loggers})
    44  	store, _ := ldcomponents.InMemoryDataStore().Build(testContext)
    45  	p.updates = mocks.NewMockDataSourceUpdates(store)
    46  	testContext.DataSourceUpdateSink = p.updates
    47  	dataSource, err := factory.Build(testContext)
    48  	if err != nil {
    49  		panic(err)
    50  	}
    51  	defer dataSource.Close()
    52  	p.dataSource = dataSource
    53  	action(p)
    54  }
    55  
    56  func withTempDir(action func(dirPath string)) {
    57  	// We should put the temp files in their own directory; otherwise, we might be running a file watcher over
    58  	// all of /tmp, which is not a great idea
    59  	path, err := os.MkdirTemp("", "watched-file-data-source-test")
    60  	if err != nil {
    61  		panic(err)
    62  	}
    63  	defer os.RemoveAll(path)
    64  	action(path)
    65  }
    66  
    67  func makeTempFile(dirPath, initialText string) string {
    68  	f, err := os.CreateTemp(dirPath, "file-source-test")
    69  	if err != nil {
    70  		panic(err)
    71  	}
    72  	f.WriteString(initialText)
    73  	err = f.Close()
    74  	if err != nil {
    75  		panic(err)
    76  	}
    77  	return f.Name()
    78  }
    79  
    80  func replaceFileContents(filename string, text string) {
    81  	f, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0600)
    82  	if err != nil {
    83  		panic(err)
    84  	}
    85  	f.WriteString(text)
    86  	err = f.Sync()
    87  	if err != nil {
    88  		panic(err)
    89  	}
    90  	f.Close()
    91  }
    92  
    93  func requireTrueWithinDuration(t *testing.T, maxTime time.Duration, test func() bool) {
    94  	deadline := time.Now().Add(maxTime)
    95  	for {
    96  		if time.Now().After(deadline) {
    97  			require.FailNowf(t, "Did not see expected change", "waited %v", maxTime)
    98  		}
    99  		if test() {
   100  			return
   101  		}
   102  		time.Sleep(time.Millisecond * 100)
   103  	}
   104  }
   105  
   106  func hasFlag(t *testing.T, store subsystems.DataStore, key string, test func(ldmodel.FeatureFlag) bool) bool {
   107  	flagItem, err := store.Get(datakinds.Features, key)
   108  	if assert.NoError(t, err) && flagItem.Item != nil {
   109  		return test(*(flagItem.Item.(*ldmodel.FeatureFlag)))
   110  	}
   111  	return false
   112  }
   113  
   114  func TestNewWatchedFileDataSource(t *testing.T) {
   115  	withTempDir(func(tempDir string) {
   116  		filename := makeTempFile(tempDir, `
   117  ---
   118  flags: bad
   119  `)
   120  		defer os.Remove(filename)
   121  
   122  		factory := ldfiledata.DataSource().
   123  			FilePaths(filename).
   124  			Reloader(WatchFiles)
   125  		withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
   126  			p.dataSource.Start(p.closeWhenReady)
   127  
   128  			// Create the flags file after we start
   129  			time.Sleep(time.Second)
   130  			replaceFileContents(filename, `
   131  ---
   132  flags:
   133    my-flag:
   134      "on": true
   135  `)
   136  
   137  			<-p.closeWhenReady
   138  
   139  			// Don't use waitForExpectedChange here because the expectation is that as soon as the dataSource
   140  			// reports being ready (which it will only do once we've given it a valid file), the flag should
   141  			// be available immediately.
   142  			assert.True(t, hasFlag(t, p.updates.DataStore, "my-flag", func(f ldmodel.FeatureFlag) bool {
   143  				return f.On
   144  			}))
   145  			assert.True(t, p.dataSource.IsInitialized())
   146  
   147  			// Update the file
   148  			replaceFileContents(filename, `
   149  ---
   150  flags:
   151    my-flag:
   152      "on": false
   153  `)
   154  
   155  			requireTrueWithinDuration(t, time.Second, func() bool {
   156  				return hasFlag(t, p.updates.DataStore, "my-flag", func(f ldmodel.FeatureFlag) bool {
   157  					return !f.On
   158  				})
   159  			})
   160  			p.mockLog.AssertMessageMatch(t, true, ldlog.Info, "Reloading flag data after detecting a change")
   161  		})
   162  	})
   163  }
   164  
   165  // File need not exist when the dataSource is started
   166  func TestNewWatchedFileMissing(t *testing.T) {
   167  	withTempDir(func(tempDir string) {
   168  		filename := makeTempFile(tempDir, "")
   169  		require.NoError(t, os.Remove(filename))
   170  		defer os.Remove(filename)
   171  
   172  		factory := ldfiledata.DataSource().
   173  			FilePaths(filename).
   174  			Reloader(WatchFiles)
   175  		withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
   176  			p.dataSource.Start(p.closeWhenReady)
   177  
   178  			time.Sleep(time.Second)
   179  			replaceFileContents(filename, `
   180  ---
   181  flags:
   182    my-flag:
   183      "on": true
   184  `)
   185  
   186  			<-p.closeWhenReady
   187  
   188  			requireTrueWithinDuration(t, time.Second, func() bool {
   189  				return hasFlag(t, p.updates.DataStore, "my-flag", func(f ldmodel.FeatureFlag) bool {
   190  					return f.On
   191  				})
   192  			})
   193  			assert.True(t, p.dataSource.IsInitialized())
   194  		})
   195  	})
   196  }
   197  
   198  // Directory needn't exist when the dataSource is started
   199  func TestNewWatchedDirectoryMissing(t *testing.T) {
   200  	withTempDir(func(tempDir string) {
   201  		tempDir, err := os.MkdirTemp("", "file-source-test")
   202  		require.NoError(t, err)
   203  		defer os.RemoveAll(tempDir)
   204  
   205  		dirPath := path.Join(tempDir, "test")
   206  		filePath := path.Join(dirPath, "flags.yml")
   207  
   208  		factory := ldfiledata.DataSource().
   209  			FilePaths(filePath).
   210  			Reloader(WatchFiles)
   211  		withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
   212  			p.dataSource.Start(p.closeWhenReady)
   213  
   214  			time.Sleep(time.Second)
   215  			err = os.Mkdir(dirPath, 0700)
   216  			require.NoError(t, err)
   217  
   218  			time.Sleep(time.Second)
   219  			replaceFileContents(filePath, `
   220  ---
   221  flags:
   222    my-flag:
   223      "on": true
   224  `)
   225  
   226  			<-p.closeWhenReady
   227  
   228  			requireTrueWithinDuration(t, time.Second*2, func() bool {
   229  				return hasFlag(t, p.updates.DataStore, "my-flag", func(f ldmodel.FeatureFlag) bool {
   230  					return f.On
   231  				})
   232  			})
   233  			assert.True(t, p.dataSource.IsInitialized())
   234  		})
   235  	})
   236  }
   237  

View as plain text