| 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 | } |