github.com/letsencrypt/boulder@v0.20251208.0/ratelimits/limit_test.go (about) 1 package ratelimits 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "net/netip" 8 "os" 9 "path/filepath" 10 "slices" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/prometheus/client_golang/prometheus" 16 io_prometheus_client "github.com/prometheus/client_model/go" 17 18 "github.com/letsencrypt/boulder/config" 19 "github.com/letsencrypt/boulder/core" 20 "github.com/letsencrypt/boulder/identifier" 21 blog "github.com/letsencrypt/boulder/log" 22 "github.com/letsencrypt/boulder/metrics" 23 "github.com/letsencrypt/boulder/test" 24 ) 25 26 // loadAndParseDefaultLimits is a helper that calls both loadDefaults and 27 // parseDefaultLimits to handle a YAML file. 28 // 29 // TODO(#7901): Update the tests to test these functions individually. 30 func loadAndParseDefaultLimits(path string) (Limits, error) { 31 fromFile, err := loadDefaultsFromFile(path) 32 if err != nil { 33 return nil, err 34 } 35 36 return parseDefaultLimits(fromFile) 37 } 38 39 // loadAndParseOverrideLimitsFromFile is a helper that calls both 40 // loadOverridesFromFile and parseOverrideLimits to handle a YAML file. 41 // 42 // TODO(#7901): Update the tests to test these functions individually. 43 func loadAndParseOverrideLimitsFromFile(path string) (Limits, error) { 44 fromFile, err := loadOverridesFromFile(path) 45 if err != nil { 46 return nil, err 47 } 48 49 return parseOverrideLimits(fromFile) 50 } 51 52 func TestParseOverrideNameId(t *testing.T) { 53 // 'enum:ipv4' 54 // Valid IPv4 address. 55 name, id, err := parseOverrideNameId(NewRegistrationsPerIPAddress.String() + ":10.0.0.1") 56 test.AssertNotError(t, err, "should not error") 57 test.AssertEquals(t, name, NewRegistrationsPerIPAddress) 58 test.AssertEquals(t, id, "10.0.0.1") 59 60 // 'enum:ipv6range' 61 // Valid IPv6 address range. 62 name, id, err = parseOverrideNameId(NewRegistrationsPerIPv6Range.String() + ":2602:80a:6000::/48") 63 test.AssertNotError(t, err, "should not error") 64 test.AssertEquals(t, name, NewRegistrationsPerIPv6Range) 65 test.AssertEquals(t, id, "2602:80a:6000::/48") 66 67 // Missing colon (this should never happen but we should avoid panicking). 68 _, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + "10.0.0.1") 69 test.AssertError(t, err, "missing colon") 70 71 // Empty string. 72 _, _, err = parseOverrideNameId("") 73 test.AssertError(t, err, "empty string") 74 75 // Only a colon. 76 _, _, err = parseOverrideNameId(NewRegistrationsPerIPAddress.String() + ":") 77 test.AssertError(t, err, "only a colon") 78 79 // Invalid enum. 80 _, _, err = parseOverrideNameId("lol:noexist") 81 test.AssertError(t, err, "invalid enum") 82 } 83 84 func TestParseOverrideNameEnumId(t *testing.T) { 85 t.Parallel() 86 87 tests := []struct { 88 name string 89 input string 90 wantLimit Name 91 wantId string 92 expectError bool 93 }{ 94 { 95 name: "valid IPv4 address", 96 input: NewRegistrationsPerIPAddress.EnumString() + ":10.0.0.1", 97 wantLimit: NewRegistrationsPerIPAddress, 98 wantId: "10.0.0.1", 99 expectError: false, 100 }, 101 { 102 name: "valid IPv6 address range", 103 input: NewRegistrationsPerIPv6Range.EnumString() + ":2001:0db8:0000::/48", 104 wantLimit: NewRegistrationsPerIPv6Range, 105 wantId: "2001:0db8:0000::/48", 106 expectError: false, 107 }, 108 { 109 name: "missing colon", 110 input: NewRegistrationsPerIPAddress.EnumString() + "10.0.0.1", 111 expectError: true, 112 }, 113 { 114 name: "empty string", 115 input: "", 116 expectError: true, 117 }, 118 { 119 name: "only a colon", 120 input: NewRegistrationsPerIPAddress.EnumString() + ":", 121 expectError: true, 122 }, 123 { 124 name: "invalid enum", 125 input: "lol:noexist", 126 expectError: true, 127 }, 128 } 129 130 for _, tc := range tests { 131 t.Run(tc.name, func(t *testing.T) { 132 limit, id, err := parseOverrideNameEnumId(tc.input) 133 if tc.expectError { 134 if err == nil { 135 t.Errorf("expected error for input %q, but got none", tc.input) 136 } 137 } else { 138 test.AssertNotError(t, err, tc.name) 139 test.AssertEquals(t, limit, tc.wantLimit) 140 test.AssertEquals(t, id, tc.wantId) 141 } 142 }) 143 } 144 } 145 146 func TestValidateLimit(t *testing.T) { 147 err := ValidateLimit(&Limit{Burst: 1, Count: 1, Period: config.Duration{Duration: time.Second}}) 148 test.AssertNotError(t, err, "valid limit") 149 150 // All of the following are invalid. 151 for _, l := range []*Limit{ 152 {Burst: 0, Count: 1, Period: config.Duration{Duration: time.Second}}, 153 {Burst: 1, Count: 0, Period: config.Duration{Duration: time.Second}}, 154 {Burst: 1, Count: 1, Period: config.Duration{Duration: 0}}, 155 } { 156 err = ValidateLimit(l) 157 test.AssertError(t, err, "limit should be invalid") 158 } 159 } 160 161 func TestLoadAndParseOverrideLimitsFromFile(t *testing.T) { 162 // Load a single valid override limit with Id formatted as 'enum:RegId'. 163 l, err := loadAndParseOverrideLimitsFromFile("testdata/working_override.yml") 164 test.AssertNotError(t, err, "valid single override limit") 165 expectKey := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1") 166 test.AssertEquals(t, l[expectKey].Burst, int64(40)) 167 test.AssertEquals(t, l[expectKey].Count, int64(40)) 168 test.AssertEquals(t, l[expectKey].Period.Duration, time.Second) 169 170 // Load single valid override limit with a 'domainOrCIDR' Id. 171 l, err = loadAndParseOverrideLimitsFromFile("testdata/working_override_regid_domainorcidr.yml") 172 test.AssertNotError(t, err, "valid single override limit with Id of regId:domainOrCIDR") 173 expectKey = joinWithColon(CertificatesPerDomain.EnumString(), "example.com") 174 test.AssertEquals(t, l[expectKey].Burst, int64(40)) 175 test.AssertEquals(t, l[expectKey].Count, int64(40)) 176 test.AssertEquals(t, l[expectKey].Period.Duration, time.Second) 177 178 // Load multiple valid override limits with 'regId' Ids. 179 l, err = loadAndParseOverrideLimitsFromFile("testdata/working_overrides.yml") 180 test.AssertNotError(t, err, "multiple valid override limits") 181 expectKey1 := joinWithColon(NewRegistrationsPerIPAddress.EnumString(), "64.112.117.1") 182 test.AssertEquals(t, l[expectKey1].Burst, int64(40)) 183 test.AssertEquals(t, l[expectKey1].Count, int64(40)) 184 test.AssertEquals(t, l[expectKey1].Period.Duration, time.Second) 185 expectKey2 := joinWithColon(NewRegistrationsPerIPv6Range.EnumString(), "2602:80a:6000::/48") 186 test.AssertEquals(t, l[expectKey2].Burst, int64(50)) 187 test.AssertEquals(t, l[expectKey2].Count, int64(50)) 188 test.AssertEquals(t, l[expectKey2].Period.Duration, time.Second*2) 189 190 // Load multiple valid override limits with 'fqdnSet' Ids, as follows: 191 // - CertificatesPerFQDNSet:example.com 192 // - CertificatesPerFQDNSet:example.com,example.net 193 // - CertificatesPerFQDNSet:example.com,example.net,example.org 194 entryKey1 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com"})) 195 entryKey2 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net"})) 196 entryKey3 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.NewDNSSlice([]string{"example.com", "example.net", "example.org"})) 197 entryKey4 := newFQDNSetBucketKey(CertificatesPerFQDNSet, identifier.ACMEIdentifiers{ 198 identifier.NewIP(netip.MustParseAddr("2602:80a:6000::1")), 199 identifier.NewIP(netip.MustParseAddr("9.9.9.9")), 200 identifier.NewDNS("example.com"), 201 }) 202 203 l, err = loadAndParseOverrideLimitsFromFile("testdata/working_overrides_regid_fqdnset.yml") 204 test.AssertNotError(t, err, "multiple valid override limits with 'fqdnSet' Ids") 205 test.AssertEquals(t, l[entryKey1].Burst, int64(40)) 206 test.AssertEquals(t, l[entryKey1].Count, int64(40)) 207 test.AssertEquals(t, l[entryKey1].Period.Duration, time.Second) 208 test.AssertEquals(t, l[entryKey2].Burst, int64(50)) 209 test.AssertEquals(t, l[entryKey2].Count, int64(50)) 210 test.AssertEquals(t, l[entryKey2].Period.Duration, time.Second*2) 211 test.AssertEquals(t, l[entryKey3].Burst, int64(60)) 212 test.AssertEquals(t, l[entryKey3].Count, int64(60)) 213 test.AssertEquals(t, l[entryKey3].Period.Duration, time.Second*3) 214 test.AssertEquals(t, l[entryKey4].Burst, int64(60)) 215 test.AssertEquals(t, l[entryKey4].Count, int64(60)) 216 test.AssertEquals(t, l[entryKey4].Period.Duration, time.Second*4) 217 218 // Path is empty string. 219 _, err = loadAndParseOverrideLimitsFromFile("") 220 test.AssertError(t, err, "path is empty string") 221 test.Assert(t, os.IsNotExist(err), "path is empty string") 222 223 // Path to file which does not exist. 224 _, err = loadAndParseOverrideLimitsFromFile("testdata/file_does_not_exist.yml") 225 test.AssertError(t, err, "a file that does not exist ") 226 test.Assert(t, os.IsNotExist(err), "test file should not exist") 227 228 // Burst cannot be 0. 229 _, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_burst_0.yml") 230 test.AssertError(t, err, "single override limit with burst=0") 231 test.AssertContains(t, err.Error(), "invalid burst") 232 233 // Id cannot be empty. 234 _, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_empty_id.yml") 235 test.AssertError(t, err, "single override limit with empty id") 236 test.Assert(t, !os.IsNotExist(err), "test file should exist") 237 238 // Name cannot be empty. 239 _, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_empty_name.yml") 240 test.AssertError(t, err, "single override limit with empty name") 241 test.Assert(t, !os.IsNotExist(err), "test file should exist") 242 243 // Name must be a string representation of a valid Name enumeration. 244 _, err = loadAndParseOverrideLimitsFromFile("testdata/busted_override_invalid_name.yml") 245 test.AssertError(t, err, "single override limit with invalid name") 246 test.Assert(t, !os.IsNotExist(err), "test file should exist") 247 248 // Multiple entries, second entry has a bad name. 249 _, err = loadAndParseOverrideLimitsFromFile("testdata/busted_overrides_second_entry_bad_name.yml") 250 test.AssertError(t, err, "multiple override limits, second entry is bad") 251 test.Assert(t, !os.IsNotExist(err), "test file should exist") 252 253 // Multiple entries, third entry has id of "lol", instead of an IPv4 address. 254 _, err = loadAndParseOverrideLimitsFromFile("testdata/busted_overrides_third_entry_bad_id.yml") 255 test.AssertError(t, err, "multiple override limits, third entry has bad Id value") 256 test.Assert(t, !os.IsNotExist(err), "test file should exist") 257 } 258 259 func TestLoadOverrides(t *testing.T) { 260 mockLog := blog.NewMock() 261 262 tb, err := NewTransactionBuilderFromFiles("../test/config-next/ratelimit-defaults.yml", "../test/config-next/ratelimit-overrides.yml", metrics.NoopRegisterer, mockLog) 263 test.AssertNotError(t, err, "creating TransactionBuilder") 264 err = tb.loadOverrides(context.Background()) 265 test.AssertNotError(t, err, "loading overrides in TransactionBuilder") 266 overridesData, err := loadOverridesFromFile("../test/config-next/ratelimit-overrides.yml") 267 test.AssertNotError(t, err, "loading overrides from file") 268 testOverrides, err := parseOverrideLimits(overridesData) 269 test.AssertNotError(t, err, "parsing overrides") 270 271 newOverridesPerLimit := make(map[Name]float64) 272 for _, override := range testOverrides { 273 override.precompute() 274 newOverridesPerLimit[override.Name]++ 275 } 276 277 test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides) 278 279 var iom io_prometheus_client.Metric 280 281 for rlName, rlString := range nameToString { 282 err = tb.limitRegistry.overridesPerLimit.WithLabelValues(rlString).Write(&iom) 283 test.AssertNotError(t, err, fmt.Sprintf("encoding overridesPerLimit metric with label %q", rlString)) 284 test.AssertEquals(t, iom.Gauge.GetValue(), newOverridesPerLimit[rlName]) 285 } 286 287 err = tb.limitRegistry.overridesTimestamp.Write(&iom) 288 test.AssertNotError(t, err, "encoding overridesTimestamp metric") 289 test.Assert(t, int64(iom.Gauge.GetValue()) >= time.Now().Unix()-5, "overridesTimestamp too old") 290 291 // A failure loading overrides should log and return an error, and not 292 // overwrite existing overrides. 293 mockLog.Clear() 294 tb.limitRegistry.refreshOverrides = func(context.Context, prometheus.Gauge, blog.Logger) (Limits, error) { 295 return nil, errors.New("mock failure") 296 } 297 err = tb.limitRegistry.loadOverrides(context.Background()) 298 test.AssertError(t, err, "fail to load overrides") 299 test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides) 300 301 // An empty set of overrides should log a warning, return nil, and not 302 // overwrite existing overrides. 303 mockLog.Clear() 304 tb.limitRegistry.refreshOverrides = func(context.Context, prometheus.Gauge, blog.Logger) (Limits, error) { 305 return Limits{}, nil 306 } 307 err = tb.limitRegistry.loadOverrides(context.Background()) 308 test.AssertEquals(t, mockLog.GetAll()[0], "WARNING: loading overrides: no valid overrides") 309 test.AssertNotError(t, err, "load empty overrides") 310 test.AssertDeepEquals(t, tb.limitRegistry.overrides, testOverrides) 311 } 312 313 func TestNewRefresher(t *testing.T) { 314 mockLog := blog.NewMock() 315 316 reg := &limitRegistry{ 317 refreshOverrides: func(_ context.Context, _ prometheus.Gauge, logger blog.Logger) (Limits, error) { 318 logger.Info("refreshed") 319 return nil, nil 320 }, 321 logger: mockLog, 322 } 323 324 // Create and simultaneously cancel a refresher. 325 reg.NewRefresher(time.Millisecond * 2)() 326 time.Sleep(time.Millisecond * 20) 327 // The refresher should have run once, but then been cancelled before the 328 // first tick. 329 test.AssertDeepEquals(t, mockLog.GetAll(), []string{"INFO: refreshed", "WARNING: loading overrides: no valid overrides"}) 330 331 reg.NewRefresher(time.Nanosecond) 332 retries := 0 333 for retries < 5 { 334 if slices.Contains(mockLog.GetAll(), "INFO: refreshed") { 335 break 336 } 337 retries++ 338 time.Sleep(core.RetryBackoff(retries, time.Millisecond*2, time.Millisecond*50, 2)) 339 } 340 test.AssertSliceContains(t, mockLog.GetAll(), "INFO: refreshed") 341 test.Assert(t, len(mockLog.GetAll()) > 1, "refresher didn't run more than once") 342 } 343 344 func TestHydrateOverrideLimit(t *testing.T) { 345 t.Parallel() 346 347 tests := []struct { 348 name string 349 bucketKey string 350 limit Limit 351 expectBucketKey string 352 expectError string 353 }{ 354 { 355 name: "bad limit name", 356 bucketKey: "", 357 limit: Limit{Name: 37}, 358 expectBucketKey: "", 359 expectError: "unrecognized limit name 37", 360 }, 361 { 362 name: "CertificatesPerDomain with bad FQDN, should fail validateIdForName", 363 bucketKey: "VelociousVacherin", 364 limit: Limit{ 365 Name: StringToName["CertificatesPerDomain"], 366 Burst: 1, 367 Count: 1, 368 Period: config.Duration{Duration: time.Second}, 369 }, 370 expectBucketKey: "", 371 expectError: "\"VelociousVacherin\" is neither a domain (Domain name needs at least one dot) nor an IP address (ParseAddr(\"VelociousVacherin\"): unable to parse IP)", 372 }, 373 { 374 name: "CertificatesPerDomain with IPv4 address", 375 bucketKey: "64.112.117.1", 376 limit: Limit{ 377 Name: StringToName["CertificatesPerDomain"], 378 Burst: 1, 379 Count: 1, 380 Period: config.Duration{Duration: time.Second}, 381 }, 382 expectBucketKey: "64.112.117.1/32", 383 expectError: "", 384 }, 385 { 386 name: "CertificatesPerDomain with IPv6 address", 387 bucketKey: "2602:80a:6000:666::", 388 limit: Limit{ 389 Name: StringToName["CertificatesPerDomain"], 390 Burst: 1, 391 Count: 1, 392 Period: config.Duration{Duration: time.Second}, 393 }, 394 expectBucketKey: "2602:80a:6000:666::/64", 395 expectError: "", 396 }, 397 { 398 name: "CertificatesPerFQDNSet", 399 bucketKey: "example.com,example.net,example.org", 400 limit: Limit{ 401 Name: StringToName["CertificatesPerFQDNSet"], 402 Burst: 1, 403 Count: 1, 404 Period: config.Duration{Duration: time.Second}, 405 }, 406 expectBucketKey: "394e82811f52e2da38b970afdb21c9bc9af81060939c690183c00fce37408738", 407 expectError: "", 408 }, 409 } 410 411 for _, tc := range tests { 412 t.Run(tc.name, func(t *testing.T) { 413 bk, err := hydrateOverrideLimit(tc.bucketKey, tc.limit.Name) 414 if tc.expectError != "" { 415 if err == nil { 416 t.Errorf("expected error for test %q but got none", tc.name) 417 } 418 test.AssertContains(t, err.Error(), tc.expectError) 419 } else { 420 test.AssertNotError(t, err, tc.name) 421 test.AssertEquals(t, bk, tc.expectBucketKey) 422 } 423 }) 424 } 425 } 426 427 func TestLoadAndParseDefaultLimits(t *testing.T) { 428 // Load a single valid default limit. 429 l, err := loadAndParseDefaultLimits("testdata/working_default.yml") 430 test.AssertNotError(t, err, "valid single default limit") 431 test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20)) 432 test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20)) 433 test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second) 434 435 // Load multiple valid default limits. 436 l, err = loadAndParseDefaultLimits("testdata/working_defaults.yml") 437 test.AssertNotError(t, err, "multiple valid default limits") 438 test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Burst, int64(20)) 439 test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Count, int64(20)) 440 test.AssertEquals(t, l[NewRegistrationsPerIPAddress.EnumString()].Period.Duration, time.Second) 441 test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Burst, int64(30)) 442 test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Count, int64(30)) 443 test.AssertEquals(t, l[NewRegistrationsPerIPv6Range.EnumString()].Period.Duration, time.Second*2) 444 445 // Path is empty string. 446 _, err = loadAndParseDefaultLimits("") 447 test.AssertError(t, err, "path is empty string") 448 test.Assert(t, os.IsNotExist(err), "path is empty string") 449 450 // Path to file which does not exist. 451 _, err = loadAndParseDefaultLimits("testdata/file_does_not_exist.yml") 452 test.AssertError(t, err, "a file that does not exist") 453 test.Assert(t, os.IsNotExist(err), "test file should not exist") 454 455 // Burst cannot be 0. 456 _, err = loadAndParseDefaultLimits("testdata/busted_default_burst_0.yml") 457 test.AssertError(t, err, "single default limit with burst=0") 458 test.AssertContains(t, err.Error(), "invalid burst") 459 460 // Name cannot be empty. 461 _, err = loadAndParseDefaultLimits("testdata/busted_default_empty_name.yml") 462 test.AssertError(t, err, "single default limit with empty name") 463 test.Assert(t, !os.IsNotExist(err), "test file should exist") 464 465 // Name must be a string representation of a valid Name enumeration. 466 _, err = loadAndParseDefaultLimits("testdata/busted_default_invalid_name.yml") 467 test.AssertError(t, err, "single default limit with invalid name") 468 test.Assert(t, !os.IsNotExist(err), "test file should exist") 469 470 // Multiple entries, second entry has a bad name. 471 _, err = loadAndParseDefaultLimits("testdata/busted_defaults_second_entry_bad_name.yml") 472 test.AssertError(t, err, "multiple default limits, one is bad") 473 test.Assert(t, !os.IsNotExist(err), "test file should exist") 474 } 475 476 func TestLoadAndDumpOverrides(t *testing.T) { 477 t.Parallel() 478 479 input := ` 480 - CertificatesPerDomain: 481 burst: 5000 482 count: 5000 483 period: 168h0m0s 484 ids: 485 - id: example.com 486 comment: IN-10057 487 - id: example.net 488 comment: IN-10057 489 - CertificatesPerDomain: 490 burst: 300 491 count: 300 492 period: 168h0m0s 493 ids: 494 - id: example.org 495 comment: IN-10057 496 - CertificatesPerDomainPerAccount: 497 burst: 12000 498 count: 12000 499 period: 168h0m0s 500 ids: 501 - id: "123456789" 502 comment: Affluent (IN-8322) 503 - CertificatesPerDomainPerAccount: 504 burst: 6000 505 count: 6000 506 period: 168h0m0s 507 ids: 508 - id: "543219876" 509 comment: Affluent (IN-8322) 510 - id: "987654321" 511 comment: Affluent (IN-8322) 512 - CertificatesPerFQDNSet: 513 burst: 50 514 count: 50 515 period: 168h0m0s 516 ids: 517 - id: example.co.uk,example.cn 518 comment: IN-6843 519 - CertificatesPerFQDNSet: 520 burst: 24 521 count: 24 522 period: 168h0m0s 523 ids: 524 - id: example.org,example.com,example.net 525 comment: IN-6006 526 - FailedAuthorizationsPerDomainPerAccount: 527 burst: 250 528 count: 250 529 period: 1h0m0s 530 ids: 531 - id: "123456789" 532 comment: Digital Lake (IN-6736) 533 - FailedAuthorizationsPerDomainPerAccount: 534 burst: 50 535 count: 50 536 period: 1h0m0s 537 ids: 538 - id: "987654321" 539 comment: Digital Lake (IN-6856) 540 - FailedAuthorizationsPerDomainPerAccount: 541 burst: 10 542 count: 10 543 period: 1h0m0s 544 ids: 545 - id: "543219876" 546 comment: Big Mart (IN-6949) 547 - NewOrdersPerAccount: 548 burst: 3000 549 count: 3000 550 period: 3h0m0s 551 ids: 552 - id: "123456789" 553 comment: Galaxy Hoster (IN-8180) 554 - NewOrdersPerAccount: 555 burst: 1000 556 count: 1000 557 period: 3h0m0s 558 ids: 559 - id: "543219876" 560 comment: Big Mart (IN-8180) 561 - id: "987654321" 562 comment: Buy More (IN-10057) 563 - NewRegistrationsPerIPAddress: 564 burst: 100000 565 count: 100000 566 period: 3h0m0s 567 ids: 568 - id: 2600:1f1c:5e0:e702:ca06:d2a3:c7ce:a02e 569 comment: example.org IN-2395 570 - id: 55.66.77.88 571 comment: example.org IN-2395 572 - NewRegistrationsPerIPAddress: 573 burst: 200 574 count: 200 575 period: 3h0m0s 576 ids: 577 - id: 11.22.33.44 578 comment: example.net (IN-1583)` 579 580 expectCSV := ` 581 name,id,count,burst,period,comment 582 CertificatesPerDomain,example.com,5000,5000,168h0m0s,IN-10057 583 CertificatesPerDomain,example.net,5000,5000,168h0m0s,IN-10057 584 CertificatesPerDomain,example.org,300,300,168h0m0s,IN-10057 585 CertificatesPerDomainPerAccount,123456789,12000,12000,168h0m0s,Affluent (IN-8322) 586 CertificatesPerDomainPerAccount,543219876,6000,6000,168h0m0s,Affluent (IN-8322) 587 CertificatesPerDomainPerAccount,987654321,6000,6000,168h0m0s,Affluent (IN-8322) 588 CertificatesPerFQDNSet,7c956936126b492845ddb48f4d220034509e7c0ad54ed2c1ba2650406846d9c3,50,50,168h0m0s,IN-6843 589 CertificatesPerFQDNSet,394e82811f52e2da38b970afdb21c9bc9af81060939c690183c00fce37408738,24,24,168h0m0s,IN-6006 590 FailedAuthorizationsPerDomainPerAccount,123456789,250,250,1h0m0s,Digital Lake (IN-6736) 591 FailedAuthorizationsPerDomainPerAccount,987654321,50,50,1h0m0s,Digital Lake (IN-6856) 592 FailedAuthorizationsPerDomainPerAccount,543219876,10,10,1h0m0s,Big Mart (IN-6949) 593 NewOrdersPerAccount,123456789,3000,3000,3h0m0s,Galaxy Hoster (IN-8180) 594 NewOrdersPerAccount,543219876,1000,1000,3h0m0s,Big Mart (IN-8180) 595 NewOrdersPerAccount,987654321,1000,1000,3h0m0s,Buy More (IN-10057) 596 NewRegistrationsPerIPAddress,2600:1f1c:5e0:e702:ca06:d2a3:c7ce:a02e,100000,100000,3h0m0s,example.org IN-2395 597 NewRegistrationsPerIPAddress,55.66.77.88,100000,100000,3h0m0s,example.org IN-2395 598 NewRegistrationsPerIPAddress,11.22.33.44,200,200,3h0m0s,example.net (IN-1583) 599 ` 600 tempDir := t.TempDir() 601 tempFile := filepath.Join(tempDir, "overrides.yaml") 602 603 err := os.WriteFile(tempFile, []byte(input), 0644) 604 test.AssertNotError(t, err, "writing temp overrides.yaml") 605 606 original, err := LoadOverridesByBucketKey(tempFile) 607 test.AssertNotError(t, err, "loading overrides") 608 test.Assert(t, len(original) > 0, "expected at least one override loaded") 609 610 dumpFile := filepath.Join(tempDir, "dumped.yaml") 611 err = DumpOverrides(dumpFile, original) 612 test.AssertNotError(t, err, "dumping overrides") 613 614 dumped, err := os.ReadFile(dumpFile) 615 test.AssertNotError(t, err, "reading dumped overrides file") 616 test.AssertEquals(t, strings.TrimLeft(string(dumped), "\n"), strings.TrimLeft(expectCSV, "\n")) 617 }