1 package ldfiledata
2
3 import (
4 "errors"
5 "os"
6 "testing"
7
8 "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest/mocks"
9
10 "github.com/launchdarkly/go-sdk-common/v3/ldlog"
11 "github.com/launchdarkly/go-sdk-common/v3/ldlogtest"
12 "github.com/launchdarkly/go-sdk-common/v3/ldvalue"
13 "github.com/launchdarkly/go-server-sdk-evaluation/v2/ldmodel"
14 "github.com/launchdarkly/go-server-sdk/v6/interfaces"
15 "github.com/launchdarkly/go-server-sdk/v6/internal/datakinds"
16 "github.com/launchdarkly/go-server-sdk/v6/internal/sharedtest"
17 "github.com/launchdarkly/go-server-sdk/v6/ldcomponents"
18 "github.com/launchdarkly/go-server-sdk/v6/subsystems"
19
20 th "github.com/launchdarkly/go-test-helpers/v3"
21
22 "github.com/stretchr/testify/assert"
23 "github.com/stretchr/testify/require"
24 )
25
26 type fileDataSourceTestParams struct {
27 dataSource subsystems.DataSource
28 updates *mocks.MockDataSourceUpdates
29 mockLog *ldlogtest.MockLog
30 closeWhenReady chan struct{}
31 }
32
33 func (p fileDataSourceTestParams) waitForStart() {
34 p.dataSource.Start(p.closeWhenReady)
35 <-p.closeWhenReady
36 }
37
38 func withFileDataSourceTestParams(
39 factory subsystems.ComponentConfigurer[subsystems.DataSource],
40 action func(fileDataSourceTestParams),
41 ) {
42 p := fileDataSourceTestParams{}
43 mockLog := ldlogtest.NewMockLog()
44 testContext := sharedtest.NewTestContext("", nil, &subsystems.LoggingConfiguration{Loggers: mockLog.Loggers})
45 store, _ := ldcomponents.InMemoryDataStore().Build(testContext)
46 updates := mocks.NewMockDataSourceUpdates(store)
47 testContext.DataSourceUpdateSink = updates
48 dataSource, err := factory.Build(testContext)
49 if err != nil {
50 panic(err)
51 }
52 defer dataSource.Close()
53 p.dataSource = dataSource
54 action(fileDataSourceTestParams{dataSource, updates, mockLog, make(chan struct{})})
55 }
56
57 func expectCreationError(t *testing.T, factory subsystems.ComponentConfigurer[subsystems.DataSource]) error {
58 testContext := sharedtest.NewTestContext("", nil, nil)
59 store, _ := ldcomponents.InMemoryDataStore().Build(testContext)
60 updates := mocks.NewMockDataSourceUpdates(store)
61 testContext.DataSourceUpdateSink = updates
62 dataSource, err := factory.Build(testContext)
63 require.Error(t, err)
64 require.Nil(t, dataSource)
65 return err
66 }
67
68 func TestNewFileDataSourceYaml(t *testing.T) {
69 fileData := `
70 ---
71 flags:
72 my-flag:
73 "on": true
74 segments:
75 my-segment:
76 rules: []
77 `
78 th.WithTempFileData([]byte(fileData), func(filename string) {
79 factory := DataSource().FilePaths(filename)
80 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
81 p.waitForStart()
82 require.True(t, p.dataSource.IsInitialized())
83
84 flag := requireFlag(t, p.updates.DataStore, "my-flag")
85 assert.True(t, flag.On)
86
87 segment := requireSegment(t, p.updates.DataStore, "my-segment")
88 assert.Empty(t, segment.Rules)
89 })
90 })
91 }
92
93 func TestNewFileDataSourceJson(t *testing.T) {
94 th.WithTempFileData([]byte(`{"flags": {"my-flag": {"on": true}}}`), func(filename string) {
95 factory := DataSource().FilePaths(filename)
96 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
97 p.waitForStart()
98 require.True(t, p.dataSource.IsInitialized())
99
100 flag := requireFlag(t, p.updates.DataStore, "my-flag")
101 assert.True(t, flag.On)
102 })
103 })
104 }
105
106 func TestStatusIsValidAfterSuccessfulLoad(t *testing.T) {
107 th.WithTempFileData([]byte(`{"flags": {"my-flag": {"on": true}}}`), func(filename string) {
108 factory := DataSource().FilePaths(filename)
109 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
110 p.waitForStart()
111 require.True(t, p.dataSource.IsInitialized())
112
113 p.updates.RequireStatusOf(t, interfaces.DataSourceStateValid)
114 })
115 })
116 }
117
118 func TestNewFileDataSourceJsonWithTwoFiles(t *testing.T) {
119 th.WithTempFileData([]byte(`{"flags": {"my-flag1": {"on": true}}}`), func(filename1 string) {
120 th.WithTempFileData([]byte(`{"flags": {"my-flag2": {"on": true}}}`), func(filename2 string) {
121 factory := DataSource().FilePaths(filename1, filename2)
122 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
123 p.waitForStart()
124 require.True(t, p.dataSource.IsInitialized())
125
126 flag1 := requireFlag(t, p.updates.DataStore, "my-flag1")
127 assert.True(t, flag1.On)
128
129 flag2 := requireFlag(t, p.updates.DataStore, "my-flag2")
130 assert.True(t, flag2.On)
131 })
132 })
133 })
134 }
135
136 func TestNewFileDataSourceJsonWithTwoConflictingFiles(t *testing.T) {
137 file1Data := `{"flags": {"flag1": {"on": true}, "flag2": {"on": true}}, "segments": {"segment1": {}}}`
138 file2Data := `{"flags": {"flag2": {"on": true}}}`
139 file3Data := `{"flagValues": {"flag2": true}}`
140 file4Data := `{"segments": {"segment1": {}}}`
141
142 th.WithTempFileData([]byte(file1Data), func(filename1 string) {
143 for _, data := range []string{file2Data, file3Data, file4Data} {
144 th.WithTempFileData([]byte(data), func(filename2 string) {
145 factory := DataSource().FilePaths(filename1, filename2)
146 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
147 p.waitForStart()
148 require.False(t, p.dataSource.IsInitialized())
149
150 p.mockLog.AssertMessageMatch(t, true, ldlog.Error, "specified by multiple files")
151 })
152 })
153 }
154 })
155 }
156
157 func TestDuplicateKeysHandlingCanSuppressErrors(t *testing.T) {
158 file1Data := `{"flags": {"flag1": {"on": true}, "flag2": {"on": false}}, "segments": {"segment1": {}}}`
159 file2Data := `{"flags": {"flag2": {"on": true}}}`
160
161 th.WithTempFileData([]byte(file1Data), func(filename1 string) {
162 th.WithTempFileData([]byte(file2Data), func(filename2 string) {
163 factory := DataSource().FilePaths(filename1, filename2).
164 DuplicateKeysHandling(DuplicateKeysIgnoreAllButFirst)
165 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
166 p.waitForStart()
167 require.True(t, p.dataSource.IsInitialized())
168
169 flag2 := requireFlag(t, p.updates.DataStore, "flag2")
170 assert.False(t, flag2.On)
171
172 p.mockLog.AssertMessageMatch(t, false, ldlog.Error, "specified by multiple files")
173 })
174 })
175 })
176 }
177
178 func TestNewFileDataSourceBadData(t *testing.T) {
179 th.WithTempFileData([]byte(`bad data`), func(filename string) {
180 factory := DataSource().FilePaths(filename)
181 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
182 p.waitForStart()
183 require.False(t, p.dataSource.IsInitialized())
184 })
185 })
186 }
187
188 func TestNewFileDataSourceMissingFile(t *testing.T) {
189 th.WithTempFileData([]byte{}, func(filename string) {
190 os.Remove(filename)
191
192 factory := DataSource().FilePaths(filename)
193 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
194 p.waitForStart()
195 assert.False(t, p.dataSource.IsInitialized())
196 })
197 })
198 }
199
200 func TestStatusIsInterruptedAfterUnsuccessfulLoad(t *testing.T) {
201 th.WithTempFileData([]byte(`bad data`), func(filename string) {
202 factory := DataSource().FilePaths(filename)
203 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
204 p.waitForStart()
205 require.False(t, p.dataSource.IsInitialized())
206
207 p.updates.RequireStatusOf(t, interfaces.DataSourceStateInterrupted)
208 })
209 })
210 }
211
212 func TestNewFileDataSourceYamlValues(t *testing.T) {
213 fileData := `
214 ---
215 flagValues:
216 my-flag: true
217 `
218 th.WithTempFileData([]byte(fileData), func(filename string) {
219 factory := DataSource().FilePaths(filename)
220 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
221 p.waitForStart()
222 require.True(t, p.dataSource.IsInitialized())
223
224 flag := requireFlag(t, p.updates.DataStore, "my-flag")
225 assert.Equal(t, []ldvalue.Value{ldvalue.Bool(true)}, flag.Variations)
226 })
227 })
228 }
229
230 func TestReloaderFailureDoesNotPreventStarting(t *testing.T) {
231 e := errors.New("sorry")
232 f := func(paths []string, loggers ldlog.Loggers, reload func(), closeCh <-chan struct{}) error {
233 return e
234 }
235 factory := DataSource().Reloader(f)
236 withFileDataSourceTestParams(factory, func(p fileDataSourceTestParams) {
237 p.waitForStart()
238 assert.True(t, p.dataSource.IsInitialized())
239 assert.Len(t, p.mockLog.GetOutput(ldlog.Error), 1)
240 })
241 }
242
243 func requireFlag(t *testing.T, store subsystems.DataStore, key string) *ldmodel.FeatureFlag {
244 item, err := store.Get(datakinds.Features, key)
245 require.NoError(t, err)
246 require.NotNil(t, item.Item)
247 return item.Item.(*ldmodel.FeatureFlag)
248 }
249
250 func requireSegment(t *testing.T, store subsystems.DataStore, key string) *ldmodel.Segment {
251 item, err := store.Get(datakinds.Segments, key)
252 require.NoError(t, err)
253 require.NotNil(t, item.Item)
254 return item.Item.(*ldmodel.Segment)
255 }
256
View as plain text