master
go 357 lines 12.4 KB
Raw
1 package config
2
3 import (
4 "testing"
5 "time"
6
7 "github.com/stretchr/testify/assert"
8 "github.com/stretchr/testify/require"
9 )
10
11 func TestParseProvideStrategy(t *testing.T) {
12 t.Run("valid strategies", func(t *testing.T) {
13 tests := []struct {
14 input string
15 expect ProvideStrategy
16 }{
17 {"all", ProvideStrategyAll},
18 {"pinned", ProvideStrategyPinned},
19 {"roots", ProvideStrategyRoots},
20 {"mfs", ProvideStrategyMFS},
21 {"pinned+mfs", ProvideStrategyPinned | ProvideStrategyMFS},
22 {"pinned+roots", ProvideStrategyPinned | ProvideStrategyRoots},
23 {"pinned+mfs+roots", ProvideStrategyPinned | ProvideStrategyMFS | ProvideStrategyRoots},
24 {"", ProvideStrategyAll}, // empty string = default = all
25 {"flat", ProvideStrategyAll}, // deprecated, maps to "all"
26 {"flat+all", ProvideStrategyAll}, // redundant but valid
27 {"all+all", ProvideStrategyAll}, // redundant but valid
28 {"mfs+pinned", ProvideStrategyMFS | ProvideStrategyPinned}, // order doesn't matter
29 // +unique and +entities modifiers
30 {"pinned+unique", ProvideStrategyPinned | ProvideStrategyUnique},
31 {"pinned+entities", ProvideStrategyPinned | ProvideStrategyEntities | ProvideStrategyUnique},
32 {"pinned+unique+entities", ProvideStrategyPinned | ProvideStrategyUnique | ProvideStrategyEntities},
33 {"mfs+unique", ProvideStrategyMFS | ProvideStrategyUnique},
34 {"mfs+entities", ProvideStrategyMFS | ProvideStrategyEntities | ProvideStrategyUnique},
35 {"pinned+mfs+unique", ProvideStrategyPinned | ProvideStrategyMFS | ProvideStrategyUnique},
36 {"pinned+mfs+entities", ProvideStrategyPinned | ProvideStrategyMFS | ProvideStrategyEntities | ProvideStrategyUnique},
37 }
38
39 for _, tt := range tests {
40 result, err := ParseProvideStrategy(tt.input)
41 require.NoError(t, err, "ParseProvideStrategy(%q)", tt.input)
42 assert.Equal(t, tt.expect, result, "ParseProvideStrategy(%q)", tt.input)
43 }
44 })
45
46 t.Run("unknown token (including typos)", func(t *testing.T) {
47 tests := []struct {
48 input string
49 err string
50 }{
51 {"invalid", `unknown provide strategy token: "invalid"`},
52 {"uniuqe", `unknown provide strategy token: "uniuqe"`}, // typo of "unique"
53 {"entites", `unknown provide strategy token: "entites"`}, // cspell:disable-line -- intentional typo of "entities"
54 {"pinned+uniuqe", `unknown provide strategy token: "uniuqe"`}, // typo in combo
55 }
56
57 for _, tt := range tests {
58 _, err := ParseProvideStrategy(tt.input)
59 require.Error(t, err, "ParseProvideStrategy(%q) should fail", tt.input)
60 assert.Contains(t, err.Error(), tt.err)
61 }
62 })
63
64 t.Run("empty token from delimiter", func(t *testing.T) {
65 tests := []string{
66 "pinned+", // trailing +
67 "+pinned", // leading +
68 "pinned++mfs", // double +
69 }
70
71 for _, input := range tests {
72 _, err := ParseProvideStrategy(input)
73 require.Error(t, err, "ParseProvideStrategy(%q) should fail", input)
74 assert.Contains(t, err.Error(), "empty token")
75 }
76 })
77
78 t.Run("all cannot be combined with other strategies", func(t *testing.T) {
79 tests := []string{
80 "all+pinned",
81 "all+mfs",
82 "all+roots",
83 "flat+pinned",
84 "all+pinned+mfs",
85 }
86
87 for _, input := range tests {
88 _, err := ParseProvideStrategy(input)
89 require.Error(t, err, "ParseProvideStrategy(%q) should fail", input)
90 assert.Contains(t, err.Error(), "cannot be combined")
91 }
92 })
93
94 t.Run("+unique/+entities require base strategy", func(t *testing.T) {
95 tests := []string{
96 "unique", // modifier alone
97 "entities", // modifier alone
98 "unique+entities", // modifiers without base
99 "roots+unique", // roots is incompatible
100 "roots+entities", // roots is incompatible
101 "roots+pinned+unique", // roots mixed with pinned+unique
102 }
103
104 for _, input := range tests {
105 _, err := ParseProvideStrategy(input)
106 require.Error(t, err, "ParseProvideStrategy(%q) should fail", input)
107 }
108 })
109 }
110
111 func TestMustParseProvideStrategy(t *testing.T) {
112 t.Run("valid input returns strategy", func(t *testing.T) {
113 assert.Equal(t, ProvideStrategyAll, MustParseProvideStrategy("all"))
114 assert.Equal(t, ProvideStrategyPinned|ProvideStrategyMFS, MustParseProvideStrategy("pinned+mfs"))
115 })
116
117 t.Run("invalid input panics", func(t *testing.T) {
118 assert.Panics(t, func() { MustParseProvideStrategy("bogus") })
119 assert.Panics(t, func() { MustParseProvideStrategy("all+pinned") })
120 })
121 }
122
123 func TestValidateProvideConfig_Strategy(t *testing.T) {
124 t.Run("valid strategies", func(t *testing.T) {
125 for _, s := range []string{
126 "all", "pinned", "roots", "mfs", "pinned+mfs",
127 "pinned+unique", "pinned+entities", "pinned+mfs+entities",
128 } {
129 cfg := &Provide{Strategy: NewOptionalString(s)}
130 require.NoError(t, ValidateProvideConfig(cfg), "strategy=%q", s)
131 }
132 })
133
134 t.Run("default (nil) strategy is valid", func(t *testing.T) {
135 cfg := &Provide{}
136 require.NoError(t, ValidateProvideConfig(cfg))
137 })
138
139 t.Run("invalid strategy", func(t *testing.T) {
140 cfg := &Provide{Strategy: NewOptionalString("bogus")}
141 err := ValidateProvideConfig(cfg)
142 require.Error(t, err)
143 assert.Contains(t, err.Error(), "Provide.Strategy")
144 })
145
146 t.Run("all combined with others", func(t *testing.T) {
147 cfg := &Provide{Strategy: NewOptionalString("all+pinned")}
148 err := ValidateProvideConfig(cfg)
149 require.Error(t, err)
150 assert.Contains(t, err.Error(), "cannot be combined")
151 })
152 }
153
154 func TestValidateProvideConfig_Interval(t *testing.T) {
155 tests := []struct {
156 name string
157 interval time.Duration
158 enabled Flag
159 wantErr bool
160 errMsg string
161 }{
162 {"valid default (22h)", 22 * time.Hour, Default, false, ""},
163 {"valid max (48h)", 48 * time.Hour, Default, false, ""},
164 {"valid small (1h)", 1 * time.Hour, Default, false, ""},
165 {"valid zero with explicit Enabled=true", 0, True, false, ""},
166 {"valid zero with explicit Enabled=false", 0, False, false, ""},
167 {"invalid zero without explicit Provide.Enabled", 0, Default, true, "set Provide.Enabled explicitly"},
168 {"invalid over limit (49h)", 49 * time.Hour, Default, true, "must be less than or equal to DHT provider record validity"},
169 {"invalid over limit (72h)", 72 * time.Hour, Default, true, "must be less than or equal to DHT provider record validity"},
170 {"invalid negative", -1 * time.Hour, Default, true, "must be non-negative"},
171 }
172
173 for _, tt := range tests {
174 t.Run(tt.name, func(t *testing.T) {
175 cfg := &Provide{
176 Enabled: tt.enabled,
177 DHT: ProvideDHT{
178 Interval: NewOptionalDuration(tt.interval),
179 },
180 }
181
182 err := ValidateProvideConfig(cfg)
183
184 if tt.wantErr {
185 require.Error(t, err, "expected error for interval=%v", tt.interval)
186 if tt.errMsg != "" {
187 assert.Contains(t, err.Error(), tt.errMsg, "error message mismatch")
188 }
189 } else {
190 require.NoError(t, err, "unexpected error for interval=%v", tt.interval)
191 }
192 })
193 }
194 }
195
196 func TestValidateProvideConfig_BloomFPRate(t *testing.T) {
197 tests := []struct {
198 name string
199 fpRate int64
200 wantErr bool
201 errMsg string
202 }{
203 {"valid default value", DefaultProvideBloomFPRate, false, ""},
204 {"valid minimum (1M)", MinProvideBloomFPRate, false, ""},
205 {"valid high (10M)", 10_000_000, false, ""},
206 {"valid very high (100M)", 100_000_000, false, ""},
207 {"invalid below minimum (999_999)", 999_999, true, "must be >="},
208 {"invalid small (10_000)", 10_000, true, "must be >="},
209 {"invalid one", 1, true, "must be >="},
210 {"invalid zero", 0, true, "must be >="},
211 {"invalid negative", -1, true, "must be >="},
212 }
213
214 for _, tt := range tests {
215 t.Run(tt.name, func(t *testing.T) {
216 cfg := &Provide{
217 BloomFPRate: NewOptionalInteger(tt.fpRate),
218 }
219
220 err := ValidateProvideConfig(cfg)
221
222 if tt.wantErr {
223 require.Error(t, err, "expected error for fpRate=%d", tt.fpRate)
224 if tt.errMsg != "" {
225 assert.Contains(t, err.Error(), tt.errMsg, "error message mismatch")
226 }
227 } else {
228 require.NoError(t, err, "unexpected error for fpRate=%d", tt.fpRate)
229 }
230 })
231 }
232
233 t.Run("default (nil) BloomFPRate is valid", func(t *testing.T) {
234 cfg := &Provide{}
235 require.NoError(t, ValidateProvideConfig(cfg))
236 })
237 }
238
239 func TestValidateProvideConfig_MaxWorkers(t *testing.T) {
240 tests := []struct {
241 name string
242 maxWorkers int64
243 wantErr bool
244 errMsg string
245 }{
246 {"valid default", 16, false, ""},
247 {"valid high", 100, false, ""},
248 {"valid low", 1, false, ""},
249 {"invalid zero", 0, true, "must be positive"},
250 {"invalid negative", -1, true, "must be positive"},
251 }
252
253 for _, tt := range tests {
254 t.Run(tt.name, func(t *testing.T) {
255 cfg := &Provide{
256 DHT: ProvideDHT{
257 MaxWorkers: NewOptionalInteger(tt.maxWorkers),
258 },
259 }
260
261 err := ValidateProvideConfig(cfg)
262
263 if tt.wantErr {
264 require.Error(t, err, "expected error for maxWorkers=%d", tt.maxWorkers)
265 if tt.errMsg != "" {
266 assert.Contains(t, err.Error(), tt.errMsg, "error message mismatch")
267 }
268 } else {
269 require.NoError(t, err, "unexpected error for maxWorkers=%d", tt.maxWorkers)
270 }
271 })
272 }
273 }
274
275 func TestShouldProvideForStrategy(t *testing.T) {
276 t.Run("all strategy always provides", func(t *testing.T) {
277 // ProvideStrategyAll should return true regardless of flags
278 testCases := []struct{ pinned, pinnedRoot, mfs bool }{
279 {false, false, false},
280 {true, true, true},
281 {true, false, false},
282 }
283
284 for _, tc := range testCases {
285 assert.True(t, ShouldProvideForStrategy(
286 ProvideStrategyAll, tc.pinned, tc.pinnedRoot, tc.mfs))
287 }
288 })
289
290 t.Run("single strategies match only their flag", func(t *testing.T) {
291 tests := []struct {
292 name string
293 strategy ProvideStrategy
294 pinned, pinnedRoot, mfs bool
295 want bool
296 }{
297 {"pinned: matches when pinned=true", ProvideStrategyPinned, true, false, false, true},
298 {"pinned: ignores other flags", ProvideStrategyPinned, false, true, true, false},
299
300 {"roots: matches when pinnedRoot=true", ProvideStrategyRoots, false, true, false, true},
301 {"roots: ignores other flags", ProvideStrategyRoots, true, false, true, false},
302
303 {"mfs: matches when mfs=true", ProvideStrategyMFS, false, false, true, true},
304 {"mfs: ignores other flags", ProvideStrategyMFS, true, true, false, false},
305 }
306
307 for _, tt := range tests {
308 t.Run(tt.name, func(t *testing.T) {
309 got := ShouldProvideForStrategy(tt.strategy, tt.pinned, tt.pinnedRoot, tt.mfs)
310 assert.Equal(t, tt.want, got)
311 })
312 }
313 })
314
315 t.Run("combined strategies use OR logic (else-if bug fix)", func(t *testing.T) {
316 // CRITICAL: Tests the fix where bitflag combinations (pinned+mfs) didn't work
317 // because of else-if instead of separate if statements
318 tests := []struct {
319 name string
320 strategy ProvideStrategy
321 pinned, pinnedRoot, mfs bool
322 want bool
323 }{
324 // pinned|mfs: provide if EITHER matches
325 {"pinned|mfs when pinned", ProvideStrategyPinned | ProvideStrategyMFS, true, false, false, true},
326 {"pinned|mfs when mfs", ProvideStrategyPinned | ProvideStrategyMFS, false, false, true, true},
327 {"pinned|mfs when both", ProvideStrategyPinned | ProvideStrategyMFS, true, false, true, true},
328 {"pinned|mfs when neither", ProvideStrategyPinned | ProvideStrategyMFS, false, false, false, false},
329
330 // roots|mfs
331 {"roots|mfs when root", ProvideStrategyRoots | ProvideStrategyMFS, false, true, false, true},
332 {"roots|mfs when mfs", ProvideStrategyRoots | ProvideStrategyMFS, false, false, true, true},
333 {"roots|mfs when neither", ProvideStrategyRoots | ProvideStrategyMFS, false, false, false, false},
334
335 // pinned|roots
336 {"pinned|roots when pinned", ProvideStrategyPinned | ProvideStrategyRoots, true, false, false, true},
337 {"pinned|roots when root", ProvideStrategyPinned | ProvideStrategyRoots, false, true, false, true},
338 {"pinned|roots when neither", ProvideStrategyPinned | ProvideStrategyRoots, false, false, false, false},
339
340 // triple combination
341 {"all-three when any matches", ProvideStrategyPinned | ProvideStrategyRoots | ProvideStrategyMFS, false, false, true, true},
342 {"all-three when none match", ProvideStrategyPinned | ProvideStrategyRoots | ProvideStrategyMFS, false, false, false, false},
343 }
344
345 for _, tt := range tests {
346 t.Run(tt.name, func(t *testing.T) {
347 got := ShouldProvideForStrategy(tt.strategy, tt.pinned, tt.pinnedRoot, tt.mfs)
348 assert.Equal(t, tt.want, got)
349 })
350 }
351 })
352
353 t.Run("zero strategy never provides", func(t *testing.T) {
354 assert.False(t, ShouldProvideForStrategy(ProvideStrategy(0), false, false, false))
355 assert.False(t, ShouldProvideForStrategy(ProvideStrategy(0), true, true, true))
356 })
357 }