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
58
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
129 time.Sleep(time.Second)
130 replaceFileContents(filename, `
131 ---
132 flags:
133 my-flag:
134 "on": true
135 `)
136
137 <-p.closeWhenReady
138
139
140
141
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
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
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
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