github.com/m-lab/locate@v0.17.6/handler/handler_test.go (about) 1 // Package handler provides a client and handlers for responding to locate 2 // requests. 3 package handler 4 5 import ( 6 "errors" 7 "net/http" 8 "net/http/httptest" 9 "net/url" 10 "reflect" 11 "strings" 12 "testing" 13 "time" 14 15 "github.com/m-lab/go/rtx" 16 v2 "github.com/m-lab/locate/api/v2" 17 "github.com/m-lab/locate/clientgeo" 18 "github.com/m-lab/locate/heartbeat" 19 "github.com/m-lab/locate/heartbeat/heartbeattest" 20 "github.com/m-lab/locate/limits" 21 "github.com/m-lab/locate/proxy" 22 "github.com/m-lab/locate/static" 23 prom "github.com/prometheus/client_golang/api/prometheus/v1" 24 log "github.com/sirupsen/logrus" 25 "gopkg.in/square/go-jose.v2/jwt" 26 ) 27 28 func init() { 29 // Disable most logs for unit tests. 30 log.SetLevel(log.FatalLevel) 31 } 32 33 type fakeSigner struct { 34 err error 35 } 36 37 func (s *fakeSigner) Sign(cl jwt.Claims) (string, error) { 38 if s.err != nil { 39 return "", s.err 40 } 41 t := strings.Join([]string{ 42 cl.Audience[0], cl.Subject, cl.Issuer, cl.Expiry.Time().Format(time.RFC3339), 43 }, "--") 44 return t, nil 45 } 46 47 type fakeLocatorV2 struct { 48 heartbeat.StatusTracker 49 err error 50 targets []v2.Target 51 urls []url.URL 52 } 53 54 func (l *fakeLocatorV2) Nearest(service string, lat, lon float64, opts *heartbeat.NearestOptions) (*heartbeat.TargetInfo, error) { 55 if l.err != nil { 56 return nil, l.err 57 } 58 return &heartbeat.TargetInfo{ 59 Targets: l.targets, 60 URLs: l.urls, 61 Ranks: map[string]int{}, 62 }, nil 63 } 64 65 type fakeAppEngineLocator struct { 66 loc *clientgeo.Location 67 err error 68 } 69 70 func (l *fakeAppEngineLocator) Locate(req *http.Request) (*clientgeo.Location, error) { 71 return l.loc, l.err 72 } 73 74 type fakeRateLimiter struct { 75 status limits.LimitStatus 76 err error 77 } 78 79 func (r *fakeRateLimiter) IsLimited(ip, ua string) (limits.LimitStatus, error) { 80 if r.err != nil { 81 return limits.LimitStatus{}, r.err 82 } 83 return r.status, nil 84 } 85 86 func TestClient_Nearest(t *testing.T) { 87 tests := []struct { 88 name string 89 path string 90 signer Signer 91 locator *fakeLocatorV2 92 cl ClientLocator 93 project string 94 latlon string 95 limits limits.Agents 96 ipLimiter Limiter 97 header http.Header 98 wantLatLon string 99 wantKey string 100 wantStatus int 101 }{ 102 { 103 name: "error-unmatched-service", 104 path: "no-instances-serve-this/datatype-name", 105 signer: &fakeSigner{}, 106 locator: &fakeLocatorV2{ 107 err: errors.New("No servers found for this service error"), 108 }, 109 header: http.Header{ 110 "X-AppEngine-CityLatLong": []string{"40.3,-70.4"}, 111 }, 112 wantLatLon: "40.3,-70.4", // Client receives lat/lon provided by AppEngine. 113 wantStatus: http.StatusInternalServerError, 114 }, 115 { 116 name: "error-nearest-failure", 117 path: "ndt/ndt5", 118 header: http.Header{ 119 "X-AppEngine-CityLatLong": []string{"40.3,-70.4"}, 120 }, 121 wantLatLon: "40.3,-70.4", // Client receives lat/lon provided by AppEngine. 122 locator: &fakeLocatorV2{ 123 err: errors.New("Fake signer error"), 124 }, 125 wantStatus: http.StatusInternalServerError, 126 }, 127 { 128 name: "error-nearest-failure-no-content", 129 path: "ndt/ndt5", 130 locator: &fakeLocatorV2{ 131 err: heartbeat.ErrNoAvailableServers, 132 }, 133 wantStatus: http.StatusServiceUnavailable, 134 }, 135 { 136 name: "error-corrupt-latlon", 137 path: "ndt/ndt5", 138 header: http.Header{ 139 "X-AppEngine-CityLatLong": []string{"corrupt-value"}, 140 }, 141 wantStatus: http.StatusServiceUnavailable, 142 }, 143 { 144 name: "error-cannot-parse-latlon", 145 path: "ndt/ndt5", 146 cl: &fakeAppEngineLocator{ 147 loc: &clientgeo.Location{ 148 Latitude: "invalid-float", 149 Longitude: "invalid-float", 150 }, 151 }, 152 wantStatus: http.StatusInternalServerError, 153 }, 154 { 155 name: "error-limit-request", 156 path: "ndt/ndt5", 157 limits: limits.Agents{ 158 "foo": limits.NewCron("* * * * *", time.Minute), 159 }, 160 header: http.Header{ 161 "User-Agent": []string{"foo"}, 162 }, 163 wantStatus: http.StatusTooManyRequests, 164 }, 165 { 166 name: "success-nearest-server", 167 path: "ndt/ndt5", 168 signer: &fakeSigner{}, 169 locator: &fakeLocatorV2{ 170 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 171 urls: []url.URL{ 172 {Scheme: "ws", Host: ":3001", Path: "/ndt_protocol"}, 173 {Scheme: "wss", Host: ":3010", Path: "ndt_protocol"}, 174 }, 175 }, 176 header: http.Header{ 177 "X-AppEngine-CityLatLong": []string{"40.3,-70.4"}, 178 }, 179 wantLatLon: "40.3,-70.4", // Client receives lat/lon provided by AppEngine. 180 wantKey: "ws://:3001/ndt_protocol", 181 wantStatus: http.StatusOK, 182 }, 183 { 184 name: "success-nearest-server-using-region", 185 path: "ndt/ndt5", 186 signer: &fakeSigner{}, 187 locator: &fakeLocatorV2{ 188 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 189 urls: []url.URL{ 190 {Scheme: "ws", Host: ":3001", Path: "/ndt_protocol"}, 191 {Scheme: "wss", Host: ":3010", Path: "ndt_protocol"}, 192 }, 193 }, 194 header: http.Header{ 195 "X-AppEngine-Country": []string{"US"}, 196 "X-AppEngine-Region": []string{"ny"}, 197 }, 198 wantLatLon: "43.19880000,-75.3242000", // Region center. 199 wantKey: "ws://:3001/ndt_protocol", 200 wantStatus: http.StatusOK, 201 }, 202 { 203 name: "success-nearest-server-using-country", 204 path: "ndt/ndt5", 205 signer: &fakeSigner{}, 206 locator: &fakeLocatorV2{ 207 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 208 urls: []url.URL{ 209 {Scheme: "ws", Host: ":3001", Path: "/ndt_protocol"}, 210 {Scheme: "wss", Host: ":3010", Path: "ndt_protocol"}, 211 }, 212 }, 213 header: http.Header{ 214 "X-AppEngine-Region": []string{"fake-region"}, 215 "X-AppEngine-Country": []string{"US"}, 216 "X-AppEngine-CityLatLong": []string{"0.000000,0.000000"}, 217 }, 218 wantLatLon: "37.09024,-95.712891", // Country center. 219 wantKey: "ws://:3001/ndt_protocol", 220 wantStatus: http.StatusOK, 221 }, 222 { 223 name: "error-rate-limit-exceeded-ip", 224 path: "ndt/ndt5", 225 signer: &fakeSigner{}, 226 locator: &fakeLocatorV2{ 227 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 228 }, 229 header: http.Header{ 230 "X-Forwarded-For": []string{"192.0.2.1"}, 231 "User-Agent": []string{"test-client"}, 232 }, 233 ipLimiter: &fakeRateLimiter{ 234 status: limits.LimitStatus{ 235 IsLimited: true, 236 LimitType: "ip", 237 }, 238 }, 239 wantStatus: http.StatusTooManyRequests, 240 }, 241 { 242 name: "error-rate-limit-exceeded-ipua", 243 path: "ndt/ndt5", 244 signer: &fakeSigner{}, 245 locator: &fakeLocatorV2{ 246 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 247 }, 248 header: http.Header{ 249 "X-Forwarded-For": []string{"192.0.2.1"}, 250 "User-Agent": []string{"test-client"}, 251 }, 252 ipLimiter: &fakeRateLimiter{ 253 status: limits.LimitStatus{ 254 IsLimited: true, 255 LimitType: "ipua", 256 }, 257 }, 258 wantStatus: http.StatusTooManyRequests, 259 }, 260 { 261 name: "success-rate-limit-not-exceeded", 262 path: "ndt/ndt5", 263 signer: &fakeSigner{}, 264 locator: &fakeLocatorV2{ 265 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 266 urls: []url.URL{ 267 {Scheme: "ws", Host: ":3001", Path: "/ndt_protocol"}, 268 {Scheme: "wss", Host: ":3010", Path: "ndt_protocol"}, 269 }, 270 }, 271 header: http.Header{ 272 "X-AppEngine-CityLatLong": []string{"40.3,-70.4"}, 273 "X-Forwarded-For": []string{"192.168.1.1"}, 274 "User-Agent": []string{"test-client"}, 275 }, 276 ipLimiter: &fakeRateLimiter{ 277 status: limits.LimitStatus{ 278 IsLimited: false, 279 LimitType: "", 280 }, 281 }, 282 wantLatLon: "40.3,-70.4", 283 wantKey: "ws://:3001/ndt_protocol", 284 wantStatus: http.StatusOK, 285 }, 286 { 287 name: "success-rate-limiter-error", 288 path: "ndt/ndt5", 289 signer: &fakeSigner{}, 290 locator: &fakeLocatorV2{ 291 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 292 urls: []url.URL{ 293 {Scheme: "ws", Host: ":3001", Path: "/ndt_protocol"}, 294 {Scheme: "wss", Host: ":3010", Path: "/ndt_protocol"}, 295 }, 296 }, 297 header: http.Header{ 298 "X-AppEngine-CityLatLong": []string{"40.3,-70.4"}, 299 "X-Forwarded-For": []string{"192.168.1.1"}, 300 "User-Agent": []string{"test-client"}, 301 }, 302 ipLimiter: &fakeRateLimiter{ 303 err: errors.New("redis error"), 304 }, 305 wantLatLon: "40.3,-70.4", 306 wantKey: "ws://:3001/ndt_protocol", 307 wantStatus: http.StatusOK, // Should fail open 308 }, 309 { 310 name: "success-missing-forwarded-for", 311 path: "ndt/ndt5", 312 signer: &fakeSigner{}, 313 locator: &fakeLocatorV2{ 314 targets: []v2.Target{{Machine: "mlab1-lga0t.measurement-lab.org"}}, 315 urls: []url.URL{ 316 {Scheme: "ws", Host: ":3001", Path: "/ndt_protocol"}, 317 {Scheme: "wss", Host: ":3010", Path: "/ndt_protocol"}, 318 }, 319 }, 320 header: http.Header{ 321 "X-AppEngine-CityLatLong": []string{"40.3,-70.4"}, 322 // No X-Forwarded-For 323 "User-Agent": []string{"test-client"}, 324 }, 325 ipLimiter: &fakeRateLimiter{ 326 status: limits.LimitStatus{ 327 IsLimited: false, 328 LimitType: "", 329 }, 330 }, 331 wantLatLon: "40.3,-70.4", 332 wantKey: "ws://:3001/ndt_protocol", 333 wantStatus: http.StatusOK, 334 }, 335 } 336 for _, tt := range tests { 337 t.Run(tt.name, func(t *testing.T) { 338 if tt.cl == nil { 339 tt.cl = clientgeo.NewAppEngineLocator() 340 } 341 c := NewClient(tt.project, tt.signer, tt.locator, tt.cl, prom.NewAPI(nil), tt.limits, tt.ipLimiter, nil) 342 343 mux := http.NewServeMux() 344 mux.HandleFunc("/v2/nearest/", c.Nearest) 345 srv := httptest.NewServer(mux) 346 defer srv.Close() 347 348 req, err := http.NewRequest(http.MethodGet, srv.URL+"/v2/nearest/"+tt.path+"?client_name=foo", nil) 349 rtx.Must(err, "Failed to create request") 350 req.Header = tt.header 351 352 result := &v2.NearestResult{} 353 resp, err := proxy.UnmarshalResponse(req, result) 354 if err != nil { 355 t.Fatalf("Failed to get response from: %s %s", srv.URL, tt.path) 356 } 357 if resp.Header.Get("Access-Control-Allow-Origin") != "*" { 358 t.Errorf("Nearest() wrong Access-Control-Allow-Origin header; got %s, want '*'", 359 resp.Header.Get("Access-Control-Allow-Origin")) 360 } 361 if resp.Header.Get("Content-Type") != "application/json" { 362 t.Errorf("Nearest() wrong Content-Type header; got %s, want 'application/json'", 363 resp.Header.Get("Content-Type")) 364 } 365 if resp.Header.Get("X-Locate-ClientLatLon") != tt.wantLatLon { 366 t.Errorf("Nearest() wrong X-Locate-ClientLatLon header; got %s, want '%s'", 367 resp.Header.Get("X-Locate-ClientLatLon"), tt.wantLatLon) 368 } 369 if result.Error != nil && result.Error.Status != tt.wantStatus { 370 t.Errorf("Nearest() wrong status; got %d, want %d", result.Error.Status, tt.wantStatus) 371 } 372 if result.Error != nil { 373 return 374 } 375 if result.Results == nil && tt.wantStatus == http.StatusOK { 376 t.Errorf("Nearest() wrong status; got %d, want %d", result.Error.Status, tt.wantStatus) 377 } 378 if len(tt.locator.targets) != len(result.Results) { 379 t.Errorf("Nearest() wrong result count; got %d, want %d", 380 len(result.Results), len(tt.locator.targets)) 381 } 382 if len(result.Results[0].URLs) != len(static.Configs[tt.path]) { 383 t.Errorf("Nearest() result wrong URL count; got %d, want %d", 384 len(result.Results[0].URLs), len(static.Configs[tt.path])) 385 } 386 if _, ok := result.Results[0].URLs[tt.wantKey]; !ok { 387 t.Errorf("Nearest() result missing URLs key; want %q", tt.wantKey) 388 } 389 }) 390 } 391 } 392 393 func TestNewClientDirect(t *testing.T) { 394 t.Run("success", func(t *testing.T) { 395 c := NewClientDirect("fake-project", nil, nil, nil, nil) 396 if c == nil { 397 t.Error("got nil client!") 398 } 399 }) 400 } 401 402 func TestClient_Ready(t *testing.T) { 403 tests := []struct { 404 name string 405 fakeErr error 406 wantStatus int 407 }{ 408 { 409 name: "success", 410 wantStatus: http.StatusOK, 411 }, 412 { 413 name: "error-not-ready", 414 fakeErr: errors.New("fake error"), 415 wantStatus: http.StatusInternalServerError, 416 }, 417 } 418 for _, tt := range tests { 419 t.Run(tt.name, func(t *testing.T) { 420 c := NewClient("foo", &fakeSigner{}, &fakeLocatorV2{StatusTracker: &heartbeattest.FakeStatusTracker{Err: tt.fakeErr}}, nil, nil, nil, nil, nil) 421 422 mux := http.NewServeMux() 423 mux.HandleFunc("/ready/", c.Ready) 424 mux.HandleFunc("/live/", c.Live) 425 srv := httptest.NewServer(mux) 426 defer srv.Close() 427 428 req, err := http.NewRequest(http.MethodGet, srv.URL+"/ready", nil) 429 rtx.Must(err, "Failed to create request") 430 resp, err := http.DefaultClient.Do(req) 431 rtx.Must(err, "failed to issue request") 432 if resp.StatusCode != tt.wantStatus { 433 t.Errorf("Ready() wrong status; got %d; want %d", resp.StatusCode, tt.wantStatus) 434 } 435 defer resp.Body.Close() 436 437 req, err = http.NewRequest(http.MethodGet, srv.URL+"/live", nil) 438 rtx.Must(err, "Failed to create request") 439 resp, err = http.DefaultClient.Do(req) 440 rtx.Must(err, "failed to issue request") 441 if resp.StatusCode != http.StatusOK { 442 t.Errorf("Live() wrong status; got %d; want %d", resp.StatusCode, http.StatusOK) 443 } 444 defer resp.Body.Close() 445 }) 446 } 447 } 448 func TestClient_Registrations(t *testing.T) { 449 tests := []struct { 450 name string 451 instances map[string]v2.HeartbeatMessage 452 fakeErr error 453 wantStatus int 454 }{ 455 { 456 name: "success-status-200", 457 instances: map[string]v2.HeartbeatMessage{ 458 "ndt-mlab1-abc0t.mlab-sandbox.measurement-lab.org": {}, 459 }, 460 wantStatus: http.StatusOK, 461 }, 462 { 463 name: "error-status-500", 464 instances: map[string]v2.HeartbeatMessage{ 465 "invalid-hostname.xyz": {}, 466 }, 467 fakeErr: errors.New("fake error"), 468 wantStatus: http.StatusInternalServerError, 469 }, 470 } 471 for _, tt := range tests { 472 fakeStatusTracker := &heartbeattest.FakeStatusTracker{ 473 Err: tt.fakeErr, 474 FakeInstances: tt.instances, 475 } 476 477 t.Run(tt.name, func(t *testing.T) { 478 c := NewClient("foo", &fakeSigner{}, &fakeLocatorV2{StatusTracker: fakeStatusTracker}, nil, nil, nil, nil, nil) 479 480 mux := http.NewServeMux() 481 mux.HandleFunc("/v2/siteinfo/registrations/", c.Registrations) 482 srv := httptest.NewServer(mux) 483 defer srv.Close() 484 485 req, err := http.NewRequest(http.MethodGet, srv.URL+"/v2/siteinfo/registrations?org=mlab", nil) 486 rtx.Must(err, "failed to create request") 487 resp, err := http.DefaultClient.Do(req) 488 rtx.Must(err, "failed to issue request") 489 if resp.StatusCode != tt.wantStatus { 490 t.Errorf("Registrations() wrong status; got %d; want %d", resp.StatusCode, tt.wantStatus) 491 } 492 }) 493 } 494 } 495 496 func TestExtraParams(t *testing.T) { 497 tests := []struct { 498 name string 499 hostname string 500 index int 501 p paramOpts 502 client *Client 503 earlyExitProbability float64 504 want url.Values 505 }{ 506 { 507 name: "all-params", 508 hostname: "host", 509 index: 0, 510 p: paramOpts{ 511 raw: map[string][]string{"client_name": {"client"}}, 512 version: "v2", 513 ranks: map[string]int{"host": 0}, 514 svcParams: map[string]float64{}, 515 }, 516 client: &Client{}, 517 want: url.Values{ 518 "client_name": []string{"client"}, 519 "locate_version": []string{"v2"}, 520 "metro_rank": []string{"0"}, 521 "index": []string{"0"}, 522 }, 523 }, 524 { 525 name: "early-exit-client-match", 526 hostname: "host", 527 index: 0, 528 p: paramOpts{ 529 raw: map[string][]string{"client_name": {"foo"}}, 530 version: "v2", 531 ranks: map[string]int{"host": 0}, 532 svcParams: map[string]float64{}, 533 }, 534 client: &Client{ 535 earlyExitClients: map[string]bool{"foo": true}, 536 }, 537 want: url.Values{ 538 "client_name": []string{"foo"}, 539 "locate_version": []string{"v2"}, 540 "metro_rank": []string{"0"}, 541 "index": []string{"0"}, 542 "early_exit": []string{"250"}, 543 }, 544 }, 545 { 546 name: "early-exit-client-no-match", 547 hostname: "host", 548 index: 0, 549 p: paramOpts{ 550 raw: map[string][]string{"client_name": {"bar"}}, 551 version: "v2", 552 ranks: map[string]int{"host": 0}, 553 svcParams: map[string]float64{}, 554 }, 555 client: &Client{ 556 earlyExitClients: map[string]bool{"foo": true}, 557 }, 558 want: url.Values{ 559 "client_name": []string{"bar"}, 560 "locate_version": []string{"v2"}, 561 "metro_rank": []string{"0"}, 562 "index": []string{"0"}, 563 }, 564 }, 565 { 566 name: "no-client", 567 hostname: "host", 568 index: 0, 569 p: paramOpts{ 570 version: "v2", 571 ranks: map[string]int{"host": 0}, 572 svcParams: map[string]float64{}, 573 }, 574 want: url.Values{ 575 "locate_version": []string{"v2"}, 576 "metro_rank": []string{"0"}, 577 "index": []string{"0"}, 578 }, 579 }, 580 { 581 name: "unmatched-host", 582 hostname: "host", 583 index: 0, 584 p: paramOpts{ 585 version: "v2", 586 ranks: map[string]int{"different-host": 0}, 587 svcParams: map[string]float64{}, 588 }, 589 want: url.Values{ 590 "locate_version": []string{"v2"}, 591 "index": []string{"0"}, 592 }, 593 }, 594 { 595 name: "early-exit-true", 596 index: 0, 597 p: paramOpts{ 598 raw: map[string][]string{static.EarlyExitParameter: {"250"}}, 599 version: "v2", 600 svcParams: map[string]float64{ 601 static.EarlyExitParameter: 1, 602 }, 603 }, 604 earlyExitProbability: 1, 605 want: url.Values{ 606 static.EarlyExitParameter: []string{"250"}, 607 "locate_version": []string{"v2"}, 608 "index": []string{"0"}, 609 }, 610 }, 611 { 612 name: "early-exit-false", 613 index: 0, 614 p: paramOpts{ 615 raw: map[string][]string{static.EarlyExitParameter: {"250"}}, 616 version: "v2", 617 svcParams: map[string]float64{static.EarlyExitParameter: 0}, 618 }, 619 earlyExitProbability: 0, 620 want: url.Values{ 621 "locate_version": []string{"v2"}, 622 "index": []string{"0"}, 623 }, 624 }, 625 { 626 name: "max-cwnd-gain-and-early-exit-true", 627 index: 0, 628 p: paramOpts{ 629 raw: map[string][]string{ 630 static.EarlyExitParameter: {"250"}, 631 static.MaxCwndGainParameter: {"512"}, 632 }, 633 version: "v2", 634 svcParams: map[string]float64{ 635 static.EarlyExitParameter: 1, 636 static.MaxCwndGainParameter: 1, 637 }, 638 }, 639 earlyExitProbability: 1, 640 want: url.Values{ 641 static.EarlyExitParameter: []string{"250"}, 642 static.MaxCwndGainParameter: []string{"512"}, 643 "locate_version": []string{"v2"}, 644 "index": []string{"0"}, 645 }, 646 }, 647 { 648 name: "max-cwnd-gain-and-max-elapsed-time-true", 649 index: 0, 650 p: paramOpts{ 651 raw: map[string][]string{ 652 static.MaxCwndGainParameter: {"512"}, 653 static.MaxElapsedTimeParameter: {"5"}, 654 }, 655 version: "v2", 656 svcParams: map[string]float64{ 657 static.MaxElapsedTimeParameter: 1, 658 static.MaxCwndGainParameter: 1, 659 }, 660 }, 661 earlyExitProbability: 1, 662 want: url.Values{ 663 static.MaxCwndGainParameter: []string{"512"}, 664 static.MaxElapsedTimeParameter: []string{"5"}, 665 "locate_version": []string{"v2"}, 666 "index": []string{"0"}, 667 }, 668 }, 669 } 670 for _, tt := range tests { 671 t.Run(tt.name, func(t *testing.T) { 672 got := tt.client.extraParams(tt.hostname, tt.index, tt.p) 673 if !reflect.DeepEqual(got, tt.want) { 674 t.Errorf("extraParams() = %v, want %v", got, tt.want) 675 } 676 }) 677 } 678 } 679 680 func TestClient_limitRequest(t *testing.T) { 681 tests := []struct { 682 name string 683 limits limits.Agents 684 t time.Time 685 req *http.Request 686 want bool 687 }{ 688 { 689 name: "allowed-user-agent-allowed-time", 690 limits: limits.Agents{}, 691 t: time.Now().UTC(), 692 req: &http.Request{ 693 Header: http.Header{ 694 "User-Agent": []string{"foo"}, 695 }, 696 }, 697 want: false, 698 }, 699 { 700 name: "allowed-user-agent-limited-time", 701 limits: limits.Agents{ 702 "foo": limits.NewCron("* * * * *", time.Minute), // Every minute of every hour. 703 }, 704 t: time.Now().UTC(), 705 req: &http.Request{ 706 Header: http.Header{ 707 "User-Agent": []string{"bar"}, 708 }, 709 }, 710 want: false, 711 }, 712 { 713 name: "limited-user-agent-allowed-time", 714 limits: limits.Agents{ 715 "foo": limits.NewCron("*/30 * * * *", time.Minute), // Every 30th minute. 716 }, 717 t: time.Date(2023, time.November, 16, 19, 29, 0, 0, time.UTC), // Request at minute 29. 718 req: &http.Request{ 719 Header: http.Header{ 720 "User-Agent": []string{"foo"}, 721 }, 722 }, 723 want: false, 724 }, 725 { 726 name: "limited-user-agent-limited-time", 727 limits: limits.Agents{ 728 "foo": limits.NewCron("*/30 * * * *", time.Minute), // Every 30th minute. 729 }, 730 t: time.Date(2023, time.November, 16, 19, 30, 0, 0, time.UTC), // Request at minute 30. 731 req: &http.Request{ 732 Header: http.Header{ 733 "User-Agent": []string{"foo"}, 734 }, 735 }, 736 want: true, 737 }, 738 } 739 for _, tt := range tests { 740 t.Run(tt.name, func(t *testing.T) { 741 c := &Client{ 742 agentLimits: tt.limits, 743 } 744 if got := c.limitRequest(tt.t, tt.req); got != tt.want { 745 t.Errorf("Client.limitRequest() = %v, want %v", got, tt.want) 746 } 747 }) 748 } 749 }