1// Copyright 2017 The Prometheus Authors 2// Licensed under the Apache License, Version 2.0 (the "License"); 3// you may not use this file except in compliance with the License. 4// You may obtain a copy of the License at 5// 6// http://www.apache.org/licenses/LICENSE-2.0 7// 8// Unless required by applicable law or agreed to in writing, software 9// distributed under the License is distributed on an "AS IS" BASIS, 10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11// See the License for the specific language governing permissions and 12// limitations under the License. 13 14package v1 15 16import ( 17 "context" 18 "errors" 19 "fmt" 20 "io/ioutil" 21 "math" 22 "net/http" 23 "net/http/httptest" 24 "net/url" 25 "reflect" 26 "strings" 27 "testing" 28 "time" 29 30 json "github.com/json-iterator/go" 31 32 "github.com/prometheus/common/model" 33) 34 35type apiTest struct { 36 do func() (interface{}, Warnings, error) 37 inWarnings []string 38 inErr error 39 inStatusCode int 40 inRes interface{} 41 42 reqPath string 43 reqParam url.Values 44 reqMethod string 45 res interface{} 46 warnings Warnings 47 err error 48} 49 50type apiTestClient struct { 51 *testing.T 52 curTest apiTest 53} 54 55func (c *apiTestClient) URL(ep string, args map[string]string) *url.URL { 56 path := ep 57 for k, v := range args { 58 path = strings.Replace(path, ":"+k, v, -1) 59 } 60 u := &url.URL{ 61 Host: "test:9090", 62 Path: path, 63 } 64 return u 65} 66 67func (c *apiTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, Warnings, error) { 68 69 test := c.curTest 70 71 if req.URL.Path != test.reqPath { 72 c.Errorf("unexpected request path: want %s, got %s", test.reqPath, req.URL.Path) 73 } 74 if req.Method != test.reqMethod { 75 c.Errorf("unexpected request method: want %s, got %s", test.reqMethod, req.Method) 76 } 77 78 b, err := json.Marshal(test.inRes) 79 if err != nil { 80 c.Fatal(err) 81 } 82 83 resp := &http.Response{} 84 if test.inStatusCode != 0 { 85 resp.StatusCode = test.inStatusCode 86 } else if test.inErr != nil { 87 resp.StatusCode = http.StatusUnprocessableEntity 88 } else { 89 resp.StatusCode = http.StatusOK 90 } 91 92 return resp, b, test.inWarnings, test.inErr 93} 94 95func (c *apiTestClient) DoGetFallback(ctx context.Context, u *url.URL, args url.Values) (*http.Response, []byte, Warnings, error) { 96 req, err := http.NewRequest(http.MethodPost, u.String(), strings.NewReader(args.Encode())) 97 if err != nil { 98 return nil, nil, nil, err 99 } 100 return c.Do(ctx, req) 101} 102 103func TestAPIs(t *testing.T) { 104 105 testTime := time.Now() 106 107 tc := &apiTestClient{ 108 T: t, 109 } 110 promAPI := &httpAPI{ 111 client: tc, 112 } 113 114 doAlertManagers := func() func() (interface{}, Warnings, error) { 115 return func() (interface{}, Warnings, error) { 116 v, err := promAPI.AlertManagers(context.Background()) 117 return v, nil, err 118 } 119 } 120 121 doCleanTombstones := func() func() (interface{}, Warnings, error) { 122 return func() (interface{}, Warnings, error) { 123 return nil, nil, promAPI.CleanTombstones(context.Background()) 124 } 125 } 126 127 doConfig := func() func() (interface{}, Warnings, error) { 128 return func() (interface{}, Warnings, error) { 129 v, err := promAPI.Config(context.Background()) 130 return v, nil, err 131 } 132 } 133 134 doDeleteSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, Warnings, error) { 135 return func() (interface{}, Warnings, error) { 136 return nil, nil, promAPI.DeleteSeries(context.Background(), []string{matcher}, startTime, endTime) 137 } 138 } 139 140 doFlags := func() func() (interface{}, Warnings, error) { 141 return func() (interface{}, Warnings, error) { 142 v, err := promAPI.Flags(context.Background()) 143 return v, nil, err 144 } 145 } 146 147 doBuildinfo := func() func() (interface{}, Warnings, error) { 148 return func() (interface{}, Warnings, error) { 149 v, err := promAPI.Buildinfo(context.Background()) 150 return v, nil, err 151 } 152 } 153 154 doRuntimeinfo := func() func() (interface{}, Warnings, error) { 155 return func() (interface{}, Warnings, error) { 156 v, err := promAPI.Runtimeinfo(context.Background()) 157 return v, nil, err 158 } 159 } 160 161 doLabelNames := func(matches []string) func() (interface{}, Warnings, error) { 162 return func() (interface{}, Warnings, error) { 163 return promAPI.LabelNames(context.Background(), matches, time.Now().Add(-100*time.Hour), time.Now()) 164 } 165 } 166 167 doLabelValues := func(matches []string, label string) func() (interface{}, Warnings, error) { 168 return func() (interface{}, Warnings, error) { 169 return promAPI.LabelValues(context.Background(), label, matches, time.Now().Add(-100*time.Hour), time.Now()) 170 } 171 } 172 173 doQuery := func(q string, ts time.Time) func() (interface{}, Warnings, error) { 174 return func() (interface{}, Warnings, error) { 175 return promAPI.Query(context.Background(), q, ts) 176 } 177 } 178 179 doQueryRange := func(q string, rng Range) func() (interface{}, Warnings, error) { 180 return func() (interface{}, Warnings, error) { 181 return promAPI.QueryRange(context.Background(), q, rng) 182 } 183 } 184 185 doSeries := func(matcher string, startTime time.Time, endTime time.Time) func() (interface{}, Warnings, error) { 186 return func() (interface{}, Warnings, error) { 187 return promAPI.Series(context.Background(), []string{matcher}, startTime, endTime) 188 } 189 } 190 191 doSnapshot := func(skipHead bool) func() (interface{}, Warnings, error) { 192 return func() (interface{}, Warnings, error) { 193 v, err := promAPI.Snapshot(context.Background(), skipHead) 194 return v, nil, err 195 } 196 } 197 198 doRules := func() func() (interface{}, Warnings, error) { 199 return func() (interface{}, Warnings, error) { 200 v, err := promAPI.Rules(context.Background()) 201 return v, nil, err 202 } 203 } 204 205 doTargets := func() func() (interface{}, Warnings, error) { 206 return func() (interface{}, Warnings, error) { 207 v, err := promAPI.Targets(context.Background()) 208 return v, nil, err 209 } 210 } 211 212 doTargetsMetadata := func(matchTarget string, metric string, limit string) func() (interface{}, Warnings, error) { 213 return func() (interface{}, Warnings, error) { 214 v, err := promAPI.TargetsMetadata(context.Background(), matchTarget, metric, limit) 215 return v, nil, err 216 } 217 } 218 219 doMetadata := func(metric string, limit string) func() (interface{}, Warnings, error) { 220 return func() (interface{}, Warnings, error) { 221 v, err := promAPI.Metadata(context.Background(), metric, limit) 222 return v, nil, err 223 } 224 } 225 226 doTSDB := func() func() (interface{}, Warnings, error) { 227 return func() (interface{}, Warnings, error) { 228 v, err := promAPI.TSDB(context.Background()) 229 return v, nil, err 230 } 231 } 232 233 doQueryExemplars := func(query string, startTime time.Time, endTime time.Time) func() (interface{}, Warnings, error) { 234 return func() (interface{}, Warnings, error) { 235 v, err := promAPI.QueryExemplars(context.Background(), query, startTime, endTime) 236 return v, nil, err 237 } 238 } 239 240 queryTests := []apiTest{ 241 { 242 do: doQuery("2", testTime), 243 inRes: &queryResult{ 244 Type: model.ValScalar, 245 Result: &model.Scalar{ 246 Value: 2, 247 Timestamp: model.TimeFromUnix(testTime.Unix()), 248 }, 249 }, 250 251 reqMethod: "POST", 252 reqPath: "/api/v1/query", 253 reqParam: url.Values{ 254 "query": []string{"2"}, 255 "time": []string{testTime.Format(time.RFC3339Nano)}, 256 }, 257 res: &model.Scalar{ 258 Value: 2, 259 Timestamp: model.TimeFromUnix(testTime.Unix()), 260 }, 261 }, 262 { 263 do: doQuery("2", testTime), 264 inErr: fmt.Errorf("some error"), 265 266 reqMethod: "POST", 267 reqPath: "/api/v1/query", 268 reqParam: url.Values{ 269 "query": []string{"2"}, 270 "time": []string{testTime.Format(time.RFC3339Nano)}, 271 }, 272 err: fmt.Errorf("some error"), 273 }, 274 { 275 do: doQuery("2", testTime), 276 inRes: "some body", 277 inStatusCode: 500, 278 inErr: &Error{ 279 Type: ErrServer, 280 Msg: "server error: 500", 281 Detail: "some body", 282 }, 283 284 reqMethod: "POST", 285 reqPath: "/api/v1/query", 286 reqParam: url.Values{ 287 "query": []string{"2"}, 288 "time": []string{testTime.Format(time.RFC3339Nano)}, 289 }, 290 err: errors.New("server_error: server error: 500"), 291 }, 292 { 293 do: doQuery("2", testTime), 294 inRes: "some body", 295 inStatusCode: 404, 296 inErr: &Error{ 297 Type: ErrClient, 298 Msg: "client error: 404", 299 Detail: "some body", 300 }, 301 302 reqMethod: "POST", 303 reqPath: "/api/v1/query", 304 reqParam: url.Values{ 305 "query": []string{"2"}, 306 "time": []string{testTime.Format(time.RFC3339Nano)}, 307 }, 308 err: errors.New("client_error: client error: 404"), 309 }, 310 // Warning only. 311 { 312 do: doQuery("2", testTime), 313 inWarnings: []string{"warning"}, 314 inRes: &queryResult{ 315 Type: model.ValScalar, 316 Result: &model.Scalar{ 317 Value: 2, 318 Timestamp: model.TimeFromUnix(testTime.Unix()), 319 }, 320 }, 321 322 reqMethod: "POST", 323 reqPath: "/api/v1/query", 324 reqParam: url.Values{ 325 "query": []string{"2"}, 326 "time": []string{testTime.Format(time.RFC3339Nano)}, 327 }, 328 res: &model.Scalar{ 329 Value: 2, 330 Timestamp: model.TimeFromUnix(testTime.Unix()), 331 }, 332 warnings: []string{"warning"}, 333 }, 334 // Warning + error. 335 { 336 do: doQuery("2", testTime), 337 inWarnings: []string{"warning"}, 338 inRes: "some body", 339 inStatusCode: 404, 340 inErr: &Error{ 341 Type: ErrClient, 342 Msg: "client error: 404", 343 Detail: "some body", 344 }, 345 346 reqMethod: "POST", 347 reqPath: "/api/v1/query", 348 reqParam: url.Values{ 349 "query": []string{"2"}, 350 "time": []string{testTime.Format(time.RFC3339Nano)}, 351 }, 352 err: errors.New("client_error: client error: 404"), 353 warnings: []string{"warning"}, 354 }, 355 356 { 357 do: doQueryRange("2", Range{ 358 Start: testTime.Add(-time.Minute), 359 End: testTime, 360 Step: time.Minute, 361 }), 362 inErr: fmt.Errorf("some error"), 363 364 reqMethod: "POST", 365 reqPath: "/api/v1/query_range", 366 reqParam: url.Values{ 367 "query": []string{"2"}, 368 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 369 "end": []string{testTime.Format(time.RFC3339Nano)}, 370 "step": []string{time.Minute.String()}, 371 }, 372 err: fmt.Errorf("some error"), 373 }, 374 375 { 376 do: doLabelNames(nil), 377 inRes: []string{"val1", "val2"}, 378 reqMethod: "GET", 379 reqPath: "/api/v1/labels", 380 res: []string{"val1", "val2"}, 381 }, 382 { 383 do: doLabelNames(nil), 384 inRes: []string{"val1", "val2"}, 385 inWarnings: []string{"a"}, 386 reqMethod: "GET", 387 reqPath: "/api/v1/labels", 388 res: []string{"val1", "val2"}, 389 warnings: []string{"a"}, 390 }, 391 392 { 393 do: doLabelNames(nil), 394 inErr: fmt.Errorf("some error"), 395 reqMethod: "GET", 396 reqPath: "/api/v1/labels", 397 err: fmt.Errorf("some error"), 398 }, 399 { 400 do: doLabelNames(nil), 401 inErr: fmt.Errorf("some error"), 402 inWarnings: []string{"a"}, 403 reqMethod: "GET", 404 reqPath: "/api/v1/labels", 405 err: fmt.Errorf("some error"), 406 warnings: []string{"a"}, 407 }, 408 { 409 do: doLabelNames([]string{"up"}), 410 inRes: []string{"val1", "val2"}, 411 reqMethod: "GET", 412 reqPath: "/api/v1/labels", 413 reqParam: url.Values{"match[]": {"up"}}, 414 res: []string{"val1", "val2"}, 415 }, 416 417 { 418 do: doLabelValues(nil, "mylabel"), 419 inRes: []string{"val1", "val2"}, 420 reqMethod: "GET", 421 reqPath: "/api/v1/label/mylabel/values", 422 res: model.LabelValues{"val1", "val2"}, 423 }, 424 { 425 do: doLabelValues(nil, "mylabel"), 426 inRes: []string{"val1", "val2"}, 427 inWarnings: []string{"a"}, 428 reqMethod: "GET", 429 reqPath: "/api/v1/label/mylabel/values", 430 res: model.LabelValues{"val1", "val2"}, 431 warnings: []string{"a"}, 432 }, 433 434 { 435 do: doLabelValues(nil, "mylabel"), 436 inErr: fmt.Errorf("some error"), 437 reqMethod: "GET", 438 reqPath: "/api/v1/label/mylabel/values", 439 err: fmt.Errorf("some error"), 440 }, 441 { 442 do: doLabelValues(nil, "mylabel"), 443 inErr: fmt.Errorf("some error"), 444 inWarnings: []string{"a"}, 445 reqMethod: "GET", 446 reqPath: "/api/v1/label/mylabel/values", 447 err: fmt.Errorf("some error"), 448 warnings: []string{"a"}, 449 }, 450 { 451 do: doLabelValues([]string{"up"}, "mylabel"), 452 inRes: []string{"val1", "val2"}, 453 reqMethod: "GET", 454 reqPath: "/api/v1/label/mylabel/values", 455 reqParam: url.Values{"match[]": {"up"}}, 456 res: model.LabelValues{"val1", "val2"}, 457 }, 458 459 { 460 do: doSeries("up", testTime.Add(-time.Minute), testTime), 461 inRes: []map[string]string{ 462 { 463 "__name__": "up", 464 "job": "prometheus", 465 "instance": "localhost:9090"}, 466 }, 467 reqMethod: "GET", 468 reqPath: "/api/v1/series", 469 reqParam: url.Values{ 470 "match": []string{"up"}, 471 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 472 "end": []string{testTime.Format(time.RFC3339Nano)}, 473 }, 474 res: []model.LabelSet{ 475 { 476 "__name__": "up", 477 "job": "prometheus", 478 "instance": "localhost:9090", 479 }, 480 }, 481 }, 482 // Series with data + warning. 483 { 484 do: doSeries("up", testTime.Add(-time.Minute), testTime), 485 inRes: []map[string]string{ 486 { 487 "__name__": "up", 488 "job": "prometheus", 489 "instance": "localhost:9090"}, 490 }, 491 inWarnings: []string{"a"}, 492 reqMethod: "GET", 493 reqPath: "/api/v1/series", 494 reqParam: url.Values{ 495 "match": []string{"up"}, 496 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 497 "end": []string{testTime.Format(time.RFC3339Nano)}, 498 }, 499 res: []model.LabelSet{ 500 { 501 "__name__": "up", 502 "job": "prometheus", 503 "instance": "localhost:9090", 504 }, 505 }, 506 warnings: []string{"a"}, 507 }, 508 509 { 510 do: doSeries("up", testTime.Add(-time.Minute), testTime), 511 inErr: fmt.Errorf("some error"), 512 reqMethod: "GET", 513 reqPath: "/api/v1/series", 514 reqParam: url.Values{ 515 "match": []string{"up"}, 516 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 517 "end": []string{testTime.Format(time.RFC3339Nano)}, 518 }, 519 err: fmt.Errorf("some error"), 520 }, 521 // Series with error and warning. 522 { 523 do: doSeries("up", testTime.Add(-time.Minute), testTime), 524 inErr: fmt.Errorf("some error"), 525 inWarnings: []string{"a"}, 526 reqMethod: "GET", 527 reqPath: "/api/v1/series", 528 reqParam: url.Values{ 529 "match": []string{"up"}, 530 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 531 "end": []string{testTime.Format(time.RFC3339Nano)}, 532 }, 533 err: fmt.Errorf("some error"), 534 warnings: []string{"a"}, 535 }, 536 537 { 538 do: doSnapshot(true), 539 inRes: map[string]string{ 540 "name": "20171210T211224Z-2be650b6d019eb54", 541 }, 542 reqMethod: "POST", 543 reqPath: "/api/v1/admin/tsdb/snapshot", 544 reqParam: url.Values{ 545 "skip_head": []string{"true"}, 546 }, 547 res: SnapshotResult{ 548 Name: "20171210T211224Z-2be650b6d019eb54", 549 }, 550 }, 551 552 { 553 do: doSnapshot(true), 554 inErr: fmt.Errorf("some error"), 555 reqMethod: "POST", 556 reqPath: "/api/v1/admin/tsdb/snapshot", 557 err: fmt.Errorf("some error"), 558 }, 559 560 { 561 do: doCleanTombstones(), 562 reqMethod: "POST", 563 reqPath: "/api/v1/admin/tsdb/clean_tombstones", 564 }, 565 566 { 567 do: doCleanTombstones(), 568 inErr: fmt.Errorf("some error"), 569 reqMethod: "POST", 570 reqPath: "/api/v1/admin/tsdb/clean_tombstones", 571 err: fmt.Errorf("some error"), 572 }, 573 574 { 575 do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), 576 inRes: []map[string]string{ 577 { 578 "__name__": "up", 579 "job": "prometheus", 580 "instance": "localhost:9090"}, 581 }, 582 reqMethod: "POST", 583 reqPath: "/api/v1/admin/tsdb/delete_series", 584 reqParam: url.Values{ 585 "match": []string{"up"}, 586 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 587 "end": []string{testTime.Format(time.RFC3339Nano)}, 588 }, 589 }, 590 591 { 592 do: doDeleteSeries("up", testTime.Add(-time.Minute), testTime), 593 inErr: fmt.Errorf("some error"), 594 reqMethod: "POST", 595 reqPath: "/api/v1/admin/tsdb/delete_series", 596 reqParam: url.Values{ 597 "match": []string{"up"}, 598 "start": []string{testTime.Add(-time.Minute).Format(time.RFC3339Nano)}, 599 "end": []string{testTime.Format(time.RFC3339Nano)}, 600 }, 601 err: fmt.Errorf("some error"), 602 }, 603 604 { 605 do: doConfig(), 606 reqMethod: "GET", 607 reqPath: "/api/v1/status/config", 608 inRes: map[string]string{ 609 "yaml": "<content of the loaded config file in YAML>", 610 }, 611 res: ConfigResult{ 612 YAML: "<content of the loaded config file in YAML>", 613 }, 614 }, 615 616 { 617 do: doConfig(), 618 reqMethod: "GET", 619 reqPath: "/api/v1/status/config", 620 inErr: fmt.Errorf("some error"), 621 err: fmt.Errorf("some error"), 622 }, 623 624 { 625 do: doFlags(), 626 reqMethod: "GET", 627 reqPath: "/api/v1/status/flags", 628 inRes: map[string]string{ 629 "alertmanager.notification-queue-capacity": "10000", 630 "alertmanager.timeout": "10s", 631 "log.level": "info", 632 "query.lookback-delta": "5m", 633 "query.max-concurrency": "20", 634 }, 635 res: FlagsResult{ 636 "alertmanager.notification-queue-capacity": "10000", 637 "alertmanager.timeout": "10s", 638 "log.level": "info", 639 "query.lookback-delta": "5m", 640 "query.max-concurrency": "20", 641 }, 642 }, 643 644 { 645 do: doFlags(), 646 reqMethod: "GET", 647 reqPath: "/api/v1/status/flags", 648 inErr: fmt.Errorf("some error"), 649 err: fmt.Errorf("some error"), 650 }, 651 652 { 653 do: doBuildinfo(), 654 reqMethod: "GET", 655 reqPath: "/api/v1/status/buildinfo", 656 inErr: fmt.Errorf("some error"), 657 err: fmt.Errorf("some error"), 658 }, 659 660 { 661 do: doBuildinfo(), 662 reqMethod: "GET", 663 reqPath: "/api/v1/status/buildinfo", 664 inRes: map[string]interface{}{ 665 "version": "2.23.0", 666 "revision": "26d89b4b0776fe4cd5a3656dfa520f119a375273", 667 "branch": "HEAD", 668 "buildUser": "root@37609b3a0a21", 669 "buildDate": "20201126-10:56:17", 670 "goVersion": "go1.15.5", 671 }, 672 res: BuildinfoResult{ 673 Version: "2.23.0", 674 Revision: "26d89b4b0776fe4cd5a3656dfa520f119a375273", 675 Branch: "HEAD", 676 BuildUser: "root@37609b3a0a21", 677 BuildDate: "20201126-10:56:17", 678 GoVersion: "go1.15.5", 679 }, 680 }, 681 682 { 683 do: doRuntimeinfo(), 684 reqMethod: "GET", 685 reqPath: "/api/v1/status/runtimeinfo", 686 inErr: fmt.Errorf("some error"), 687 err: fmt.Errorf("some error"), 688 }, 689 690 { 691 do: doRuntimeinfo(), 692 reqMethod: "GET", 693 reqPath: "/api/v1/status/runtimeinfo", 694 inRes: map[string]interface{}{ 695 "startTime": "2020-05-18T15:52:53.4503113Z", 696 "CWD": "/prometheus", 697 "reloadConfigSuccess": true, 698 "lastConfigTime": "2020-05-18T15:52:56Z", 699 "chunkCount": 72692, 700 "timeSeriesCount": 18476, 701 "corruptionCount": 0, 702 "goroutineCount": 217, 703 "GOMAXPROCS": 2, 704 "GOGC": "100", 705 "GODEBUG": "allocfreetrace", 706 "storageRetention": "1d", 707 }, 708 res: RuntimeinfoResult{ 709 StartTime: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC), 710 CWD: "/prometheus", 711 ReloadConfigSuccess: true, 712 LastConfigTime: time.Date(2020, 5, 18, 15, 52, 56, 0, time.UTC), 713 ChunkCount: 72692, 714 TimeSeriesCount: 18476, 715 CorruptionCount: 0, 716 GoroutineCount: 217, 717 GOMAXPROCS: 2, 718 GOGC: "100", 719 GODEBUG: "allocfreetrace", 720 StorageRetention: "1d", 721 }, 722 }, 723 724 { 725 do: doAlertManagers(), 726 reqMethod: "GET", 727 reqPath: "/api/v1/alertmanagers", 728 inRes: map[string]interface{}{ 729 "activeAlertManagers": []map[string]string{ 730 { 731 "url": "http://127.0.0.1:9091/api/v1/alerts", 732 }, 733 }, 734 "droppedAlertManagers": []map[string]string{ 735 { 736 "url": "http://127.0.0.1:9092/api/v1/alerts", 737 }, 738 }, 739 }, 740 res: AlertManagersResult{ 741 Active: []AlertManager{ 742 { 743 URL: "http://127.0.0.1:9091/api/v1/alerts", 744 }, 745 }, 746 Dropped: []AlertManager{ 747 { 748 URL: "http://127.0.0.1:9092/api/v1/alerts", 749 }, 750 }, 751 }, 752 }, 753 754 { 755 do: doAlertManagers(), 756 reqMethod: "GET", 757 reqPath: "/api/v1/alertmanagers", 758 inErr: fmt.Errorf("some error"), 759 err: fmt.Errorf("some error"), 760 }, 761 762 { 763 do: doRules(), 764 reqMethod: "GET", 765 reqPath: "/api/v1/rules", 766 inRes: map[string]interface{}{ 767 "groups": []map[string]interface{}{ 768 { 769 "file": "/rules.yaml", 770 "interval": 60, 771 "name": "example", 772 "rules": []map[string]interface{}{ 773 { 774 "alerts": []map[string]interface{}{ 775 { 776 "activeAt": testTime.UTC().Format(time.RFC3339Nano), 777 "annotations": map[string]interface{}{ 778 "summary": "High request latency", 779 }, 780 "labels": map[string]interface{}{ 781 "alertname": "HighRequestLatency", 782 "severity": "page", 783 }, 784 "state": "firing", 785 "value": "1e+00", 786 }, 787 }, 788 "annotations": map[string]interface{}{ 789 "summary": "High request latency", 790 }, 791 "duration": 600, 792 "health": "ok", 793 "labels": map[string]interface{}{ 794 "severity": "page", 795 }, 796 "name": "HighRequestLatency", 797 "query": "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", 798 "type": "alerting", 799 }, 800 { 801 "health": "ok", 802 "name": "job:http_inprogress_requests:sum", 803 "query": "sum(http_inprogress_requests) by (job)", 804 "type": "recording", 805 }, 806 }, 807 }, 808 }, 809 }, 810 res: RulesResult{ 811 Groups: []RuleGroup{ 812 { 813 Name: "example", 814 File: "/rules.yaml", 815 Interval: 60, 816 Rules: []interface{}{ 817 AlertingRule{ 818 Alerts: []*Alert{ 819 { 820 ActiveAt: testTime.UTC(), 821 Annotations: model.LabelSet{ 822 "summary": "High request latency", 823 }, 824 Labels: model.LabelSet{ 825 "alertname": "HighRequestLatency", 826 "severity": "page", 827 }, 828 State: AlertStateFiring, 829 Value: "1e+00", 830 }, 831 }, 832 Annotations: model.LabelSet{ 833 "summary": "High request latency", 834 }, 835 Labels: model.LabelSet{ 836 "severity": "page", 837 }, 838 Duration: 600, 839 Health: RuleHealthGood, 840 Name: "HighRequestLatency", 841 Query: "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", 842 LastError: "", 843 }, 844 RecordingRule{ 845 Health: RuleHealthGood, 846 Name: "job:http_inprogress_requests:sum", 847 Query: "sum(http_inprogress_requests) by (job)", 848 LastError: "", 849 }, 850 }, 851 }, 852 }, 853 }, 854 }, 855 856 // This has the newer API elements like lastEvaluation, evaluationTime, etc. 857 { 858 do: doRules(), 859 reqMethod: "GET", 860 reqPath: "/api/v1/rules", 861 inRes: map[string]interface{}{ 862 "groups": []map[string]interface{}{ 863 { 864 "file": "/rules.yaml", 865 "interval": 60, 866 "name": "example", 867 "rules": []map[string]interface{}{ 868 { 869 "alerts": []map[string]interface{}{ 870 { 871 "activeAt": testTime.UTC().Format(time.RFC3339Nano), 872 "annotations": map[string]interface{}{ 873 "summary": "High request latency", 874 }, 875 "labels": map[string]interface{}{ 876 "alertname": "HighRequestLatency", 877 "severity": "page", 878 }, 879 "state": "firing", 880 "value": "1e+00", 881 }, 882 }, 883 "annotations": map[string]interface{}{ 884 "summary": "High request latency", 885 }, 886 "duration": 600, 887 "health": "ok", 888 "labels": map[string]interface{}{ 889 "severity": "page", 890 }, 891 "name": "HighRequestLatency", 892 "query": "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", 893 "type": "alerting", 894 "evaluationTime": 0.5, 895 "lastEvaluation": "2020-05-18T15:52:53.4503113Z", 896 "state": "firing", 897 }, 898 { 899 "health": "ok", 900 "name": "job:http_inprogress_requests:sum", 901 "query": "sum(http_inprogress_requests) by (job)", 902 "type": "recording", 903 "evaluationTime": 0.3, 904 "lastEvaluation": "2020-05-18T15:52:53.4503113Z", 905 }, 906 }, 907 }, 908 }, 909 }, 910 res: RulesResult{ 911 Groups: []RuleGroup{ 912 { 913 Name: "example", 914 File: "/rules.yaml", 915 Interval: 60, 916 Rules: []interface{}{ 917 AlertingRule{ 918 Alerts: []*Alert{ 919 { 920 ActiveAt: testTime.UTC(), 921 Annotations: model.LabelSet{ 922 "summary": "High request latency", 923 }, 924 Labels: model.LabelSet{ 925 "alertname": "HighRequestLatency", 926 "severity": "page", 927 }, 928 State: AlertStateFiring, 929 Value: "1e+00", 930 }, 931 }, 932 Annotations: model.LabelSet{ 933 "summary": "High request latency", 934 }, 935 Labels: model.LabelSet{ 936 "severity": "page", 937 }, 938 Duration: 600, 939 Health: RuleHealthGood, 940 Name: "HighRequestLatency", 941 Query: "job:request_latency_seconds:mean5m{job=\"myjob\"} > 0.5", 942 LastError: "", 943 EvaluationTime: 0.5, 944 LastEvaluation: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC), 945 State: "firing", 946 }, 947 RecordingRule{ 948 Health: RuleHealthGood, 949 Name: "job:http_inprogress_requests:sum", 950 Query: "sum(http_inprogress_requests) by (job)", 951 LastError: "", 952 EvaluationTime: 0.3, 953 LastEvaluation: time.Date(2020, 5, 18, 15, 52, 53, 450311300, time.UTC), 954 }, 955 }, 956 }, 957 }, 958 }, 959 }, 960 961 { 962 do: doRules(), 963 reqMethod: "GET", 964 reqPath: "/api/v1/rules", 965 inErr: fmt.Errorf("some error"), 966 err: fmt.Errorf("some error"), 967 }, 968 969 { 970 do: doTargets(), 971 reqMethod: "GET", 972 reqPath: "/api/v1/targets", 973 inRes: map[string]interface{}{ 974 "activeTargets": []map[string]interface{}{ 975 { 976 "discoveredLabels": map[string]string{ 977 "__address__": "127.0.0.1:9090", 978 "__metrics_path__": "/metrics", 979 "__scheme__": "http", 980 "job": "prometheus", 981 }, 982 "labels": map[string]string{ 983 "instance": "127.0.0.1:9090", 984 "job": "prometheus", 985 }, 986 "scrapePool": "prometheus", 987 "scrapeUrl": "http://127.0.0.1:9090", 988 "globalUrl": "http://127.0.0.1:9090", 989 "lastError": "error while scraping target", 990 "lastScrape": testTime.UTC().Format(time.RFC3339Nano), 991 "lastScrapeDuration": 0.001146115, 992 "health": "up", 993 }, 994 }, 995 "droppedTargets": []map[string]interface{}{ 996 { 997 "discoveredLabels": map[string]string{ 998 "__address__": "127.0.0.1:9100", 999 "__metrics_path__": "/metrics", 1000 "__scheme__": "http", 1001 "job": "node", 1002 }, 1003 }, 1004 }, 1005 }, 1006 res: TargetsResult{ 1007 Active: []ActiveTarget{ 1008 { 1009 DiscoveredLabels: map[string]string{ 1010 "__address__": "127.0.0.1:9090", 1011 "__metrics_path__": "/metrics", 1012 "__scheme__": "http", 1013 "job": "prometheus", 1014 }, 1015 Labels: model.LabelSet{ 1016 "instance": "127.0.0.1:9090", 1017 "job": "prometheus", 1018 }, 1019 ScrapePool: "prometheus", 1020 ScrapeURL: "http://127.0.0.1:9090", 1021 GlobalURL: "http://127.0.0.1:9090", 1022 LastError: "error while scraping target", 1023 LastScrape: testTime.UTC(), 1024 LastScrapeDuration: 0.001146115, 1025 Health: HealthGood, 1026 }, 1027 }, 1028 Dropped: []DroppedTarget{ 1029 { 1030 DiscoveredLabels: map[string]string{ 1031 "__address__": "127.0.0.1:9100", 1032 "__metrics_path__": "/metrics", 1033 "__scheme__": "http", 1034 "job": "node", 1035 }, 1036 }, 1037 }, 1038 }, 1039 }, 1040 1041 { 1042 do: doTargets(), 1043 reqMethod: "GET", 1044 reqPath: "/api/v1/targets", 1045 inErr: fmt.Errorf("some error"), 1046 err: fmt.Errorf("some error"), 1047 }, 1048 1049 { 1050 do: doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"), 1051 inRes: []map[string]interface{}{ 1052 { 1053 "target": map[string]interface{}{ 1054 "instance": "127.0.0.1:9090", 1055 "job": "prometheus", 1056 }, 1057 "type": "gauge", 1058 "help": "Number of goroutines that currently exist.", 1059 "unit": "", 1060 }, 1061 }, 1062 reqMethod: "GET", 1063 reqPath: "/api/v1/targets/metadata", 1064 reqParam: url.Values{ 1065 "match_target": []string{"{job=\"prometheus\"}"}, 1066 "metric": []string{"go_goroutines"}, 1067 "limit": []string{"1"}, 1068 }, 1069 res: []MetricMetadata{ 1070 { 1071 Target: map[string]string{ 1072 "instance": "127.0.0.1:9090", 1073 "job": "prometheus", 1074 }, 1075 Type: "gauge", 1076 Help: "Number of goroutines that currently exist.", 1077 Unit: "", 1078 }, 1079 }, 1080 }, 1081 1082 { 1083 do: doTargetsMetadata("{job=\"prometheus\"}", "go_goroutines", "1"), 1084 inErr: fmt.Errorf("some error"), 1085 reqMethod: "GET", 1086 reqPath: "/api/v1/targets/metadata", 1087 reqParam: url.Values{ 1088 "match_target": []string{"{job=\"prometheus\"}"}, 1089 "metric": []string{"go_goroutines"}, 1090 "limit": []string{"1"}, 1091 }, 1092 err: fmt.Errorf("some error"), 1093 }, 1094 1095 { 1096 do: doMetadata("go_goroutines", "1"), 1097 inRes: map[string]interface{}{ 1098 "go_goroutines": []map[string]interface{}{ 1099 { 1100 "type": "gauge", 1101 "help": "Number of goroutines that currently exist.", 1102 "unit": "", 1103 }, 1104 }, 1105 }, 1106 reqMethod: "GET", 1107 reqPath: "/api/v1/metadata", 1108 reqParam: url.Values{ 1109 "metric": []string{"go_goroutines"}, 1110 "limit": []string{"1"}, 1111 }, 1112 res: map[string][]Metadata{ 1113 "go_goroutines": []Metadata{ 1114 { 1115 Type: "gauge", 1116 Help: "Number of goroutines that currently exist.", 1117 Unit: "", 1118 }, 1119 }, 1120 }, 1121 }, 1122 1123 { 1124 do: doMetadata("", "1"), 1125 inErr: fmt.Errorf("some error"), 1126 reqMethod: "GET", 1127 reqPath: "/api/v1/metadata", 1128 reqParam: url.Values{ 1129 "metric": []string{""}, 1130 "limit": []string{"1"}, 1131 }, 1132 err: fmt.Errorf("some error"), 1133 }, 1134 1135 { 1136 do: doTSDB(), 1137 reqMethod: "GET", 1138 reqPath: "/api/v1/status/tsdb", 1139 inErr: fmt.Errorf("some error"), 1140 err: fmt.Errorf("some error"), 1141 }, 1142 1143 { 1144 do: doTSDB(), 1145 reqMethod: "GET", 1146 reqPath: "/api/v1/status/tsdb", 1147 inRes: map[string]interface{}{ 1148 "seriesCountByMetricName": []interface{}{ 1149 map[string]interface{}{ 1150 "name": "kubelet_http_requests_duration_seconds_bucket", 1151 "value": 1000, 1152 }, 1153 }, 1154 "labelValueCountByLabelName": []interface{}{ 1155 map[string]interface{}{ 1156 "name": "__name__", 1157 "value": 200, 1158 }, 1159 }, 1160 "memoryInBytesByLabelName": []interface{}{ 1161 map[string]interface{}{ 1162 "name": "id", 1163 "value": 4096, 1164 }, 1165 }, 1166 "seriesCountByLabelValuePair": []interface{}{ 1167 map[string]interface{}{ 1168 "name": "job=kubelet", 1169 "value": 30000, 1170 }, 1171 }, 1172 }, 1173 res: TSDBResult{ 1174 SeriesCountByMetricName: []Stat{ 1175 { 1176 Name: "kubelet_http_requests_duration_seconds_bucket", 1177 Value: 1000, 1178 }, 1179 }, 1180 LabelValueCountByLabelName: []Stat{ 1181 { 1182 Name: "__name__", 1183 Value: 200, 1184 }, 1185 }, 1186 MemoryInBytesByLabelName: []Stat{ 1187 { 1188 Name: "id", 1189 Value: 4096, 1190 }, 1191 }, 1192 SeriesCountByLabelValuePair: []Stat{ 1193 { 1194 Name: "job=kubelet", 1195 Value: 30000, 1196 }, 1197 }, 1198 }, 1199 }, 1200 1201 { 1202 do: doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime), 1203 reqMethod: "GET", 1204 reqPath: "/api/v1/query_exemplars", 1205 inErr: fmt.Errorf("some error"), 1206 err: fmt.Errorf("some error"), 1207 }, 1208 1209 { 1210 do: doQueryExemplars("tns_request_duration_seconds_bucket", testTime.Add(-1*time.Minute), testTime), 1211 reqMethod: "GET", 1212 reqPath: "/api/v1/query_exemplars", 1213 inRes: []interface{}{ 1214 map[string]interface{}{ 1215 "seriesLabels": map[string]interface{}{ 1216 "__name__": "tns_request_duration_seconds_bucket", 1217 "instance": "app:80", 1218 "job": "tns/app", 1219 }, 1220 "exemplars": []interface{}{ 1221 map[string]interface{}{ 1222 "labels": map[string]interface{}{ 1223 "traceID": "19fd8c8a33975a23", 1224 }, 1225 "value": "0.003863295", 1226 "timestamp": model.TimeFromUnixNano(testTime.UnixNano()), 1227 }, 1228 map[string]interface{}{ 1229 "labels": map[string]interface{}{ 1230 "traceID": "67f743f07cc786b0", 1231 }, 1232 "value": "0.001535405", 1233 "timestamp": model.TimeFromUnixNano(testTime.UnixNano()), 1234 }, 1235 }, 1236 }, 1237 }, 1238 res: []ExemplarQueryResult{ 1239 { 1240 SeriesLabels: model.LabelSet{ 1241 "__name__": "tns_request_duration_seconds_bucket", 1242 "instance": "app:80", 1243 "job": "tns/app", 1244 }, 1245 Exemplars: []Exemplar{ 1246 { 1247 Labels: model.LabelSet{"traceID": "19fd8c8a33975a23"}, 1248 Value: 0.003863295, 1249 Timestamp: model.TimeFromUnixNano(testTime.UnixNano()), 1250 }, 1251 { 1252 Labels: model.LabelSet{"traceID": "67f743f07cc786b0"}, 1253 Value: 0.001535405, 1254 Timestamp: model.TimeFromUnixNano(testTime.UnixNano()), 1255 }, 1256 }, 1257 }, 1258 }, 1259 }, 1260 } 1261 1262 var tests []apiTest 1263 tests = append(tests, queryTests...) 1264 1265 for i, test := range tests { 1266 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 1267 tc.curTest = test 1268 1269 res, warnings, err := test.do() 1270 1271 if (test.inWarnings == nil) != (warnings == nil) && !reflect.DeepEqual(test.inWarnings, warnings) { 1272 t.Fatalf("mismatch in warnings expected=%v actual=%v", test.inWarnings, warnings) 1273 } 1274 1275 if test.err != nil { 1276 if err == nil { 1277 t.Fatalf("expected error %q but got none", test.err) 1278 } 1279 if err.Error() != test.err.Error() { 1280 t.Errorf("unexpected error: want %s, got %s", test.err, err) 1281 } 1282 if apiErr, ok := err.(*Error); ok { 1283 if apiErr.Detail != test.inRes { 1284 t.Errorf("%q should be %q", apiErr.Detail, test.inRes) 1285 } 1286 } 1287 return 1288 } 1289 if err != nil { 1290 t.Fatalf("unexpected error: %s", err) 1291 } 1292 1293 if !reflect.DeepEqual(res, test.res) { 1294 t.Errorf("unexpected result: want %v, got %v", test.res, res) 1295 } 1296 }) 1297 } 1298} 1299 1300type testClient struct { 1301 *testing.T 1302 1303 ch chan apiClientTest 1304 req *http.Request 1305} 1306 1307type apiClientTest struct { 1308 code int 1309 response interface{} 1310 expectedBody string 1311 expectedErr *Error 1312 expectedWarnings Warnings 1313} 1314 1315func (c *testClient) URL(ep string, args map[string]string) *url.URL { 1316 return nil 1317} 1318 1319func (c *testClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { 1320 if ctx == nil { 1321 c.Fatalf("context was not passed down") 1322 } 1323 if req != c.req { 1324 c.Fatalf("request was not passed down") 1325 } 1326 1327 test := <-c.ch 1328 1329 var b []byte 1330 var err error 1331 1332 switch v := test.response.(type) { 1333 case string: 1334 b = []byte(v) 1335 default: 1336 b, err = json.Marshal(v) 1337 if err != nil { 1338 c.Fatal(err) 1339 } 1340 } 1341 1342 resp := &http.Response{ 1343 StatusCode: test.code, 1344 } 1345 1346 return resp, b, nil 1347} 1348 1349func TestAPIClientDo(t *testing.T) { 1350 tests := []apiClientTest{ 1351 { 1352 code: http.StatusUnprocessableEntity, 1353 response: &apiResponse{ 1354 Status: "error", 1355 Data: json.RawMessage(`null`), 1356 ErrorType: ErrBadData, 1357 Error: "failed", 1358 }, 1359 expectedErr: &Error{ 1360 Type: ErrBadData, 1361 Msg: "failed", 1362 }, 1363 expectedBody: `null`, 1364 }, 1365 { 1366 code: http.StatusUnprocessableEntity, 1367 response: &apiResponse{ 1368 Status: "error", 1369 Data: json.RawMessage(`"test"`), 1370 ErrorType: ErrTimeout, 1371 Error: "timed out", 1372 }, 1373 expectedErr: &Error{ 1374 Type: ErrTimeout, 1375 Msg: "timed out", 1376 }, 1377 expectedBody: `test`, 1378 }, 1379 { 1380 code: http.StatusInternalServerError, 1381 response: "500 error details", 1382 expectedErr: &Error{ 1383 Type: ErrServer, 1384 Msg: "server error: 500", 1385 Detail: "500 error details", 1386 }, 1387 }, 1388 { 1389 code: http.StatusNotFound, 1390 response: "404 error details", 1391 expectedErr: &Error{ 1392 Type: ErrClient, 1393 Msg: "client error: 404", 1394 Detail: "404 error details", 1395 }, 1396 }, 1397 { 1398 code: http.StatusBadRequest, 1399 response: &apiResponse{ 1400 Status: "error", 1401 Data: json.RawMessage(`null`), 1402 ErrorType: ErrBadData, 1403 Error: "end timestamp must not be before start time", 1404 }, 1405 expectedErr: &Error{ 1406 Type: ErrBadData, 1407 Msg: "end timestamp must not be before start time", 1408 }, 1409 }, 1410 { 1411 code: http.StatusUnprocessableEntity, 1412 response: "bad json", 1413 expectedErr: &Error{ 1414 Type: ErrBadResponse, 1415 Msg: "readObjectStart: expect { or n, but found b, error found in #1 byte of ...|bad json|..., bigger context ...|bad json|...", 1416 }, 1417 }, 1418 { 1419 code: http.StatusUnprocessableEntity, 1420 response: &apiResponse{ 1421 Status: "success", 1422 Data: json.RawMessage(`"test"`), 1423 }, 1424 expectedErr: &Error{ 1425 Type: ErrBadResponse, 1426 Msg: "inconsistent body for response code", 1427 }, 1428 }, 1429 { 1430 code: http.StatusUnprocessableEntity, 1431 response: &apiResponse{ 1432 Status: "success", 1433 Data: json.RawMessage(`"test"`), 1434 ErrorType: ErrTimeout, 1435 Error: "timed out", 1436 }, 1437 expectedErr: &Error{ 1438 Type: ErrBadResponse, 1439 Msg: "inconsistent body for response code", 1440 }, 1441 }, 1442 { 1443 code: http.StatusOK, 1444 response: &apiResponse{ 1445 Status: "error", 1446 Data: json.RawMessage(`"test"`), 1447 ErrorType: ErrTimeout, 1448 Error: "timed out", 1449 }, 1450 expectedErr: &Error{ 1451 Type: ErrTimeout, 1452 Msg: "timed out", 1453 }, 1454 }, 1455 { 1456 code: http.StatusOK, 1457 response: &apiResponse{ 1458 Status: "error", 1459 Data: json.RawMessage(`"test"`), 1460 ErrorType: ErrTimeout, 1461 Error: "timed out", 1462 Warnings: []string{"a"}, 1463 }, 1464 expectedErr: &Error{ 1465 Type: ErrTimeout, 1466 Msg: "timed out", 1467 }, 1468 expectedWarnings: []string{"a"}, 1469 }, 1470 } 1471 1472 tc := &testClient{ 1473 T: t, 1474 ch: make(chan apiClientTest, 1), 1475 req: &http.Request{}, 1476 } 1477 client := &apiClientImpl{ 1478 client: tc, 1479 } 1480 1481 for i, test := range tests { 1482 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 1483 1484 tc.ch <- test 1485 1486 _, body, warnings, err := client.Do(context.Background(), tc.req) 1487 1488 if test.expectedWarnings != nil { 1489 if !reflect.DeepEqual(test.expectedWarnings, warnings) { 1490 t.Fatalf("mismatch in warnings expected=%v actual=%v", test.expectedWarnings, warnings) 1491 } 1492 } else { 1493 if warnings != nil { 1494 t.Fatalf("unexpexted warnings: %v", warnings) 1495 } 1496 } 1497 1498 if test.expectedErr != nil { 1499 if err == nil { 1500 t.Fatal("expected error, but got none") 1501 } 1502 1503 if test.expectedErr.Error() != err.Error() { 1504 t.Fatalf("expected error:%v, but got:%v", test.expectedErr.Error(), err.Error()) 1505 } 1506 1507 if test.expectedErr.Detail != "" { 1508 apiErr := err.(*Error) 1509 if apiErr.Detail != test.expectedErr.Detail { 1510 t.Fatalf("expected error detail :%v, but got:%v", apiErr.Detail, test.expectedErr.Detail) 1511 } 1512 } 1513 1514 return 1515 } 1516 1517 if err != nil { 1518 t.Fatalf("unexpected error:%v", err) 1519 } 1520 if test.expectedBody != string(body) { 1521 t.Fatalf("expected body :%v, but got:%v", test.expectedBody, string(body)) 1522 } 1523 }) 1524 1525 } 1526} 1527 1528func TestSamplesJsonSerialization(t *testing.T) { 1529 tests := []struct { 1530 point model.SamplePair 1531 expected string 1532 }{ 1533 { 1534 point: model.SamplePair{0, 0}, 1535 expected: `[0,"0"]`, 1536 }, 1537 { 1538 point: model.SamplePair{1, 20}, 1539 expected: `[0.001,"20"]`, 1540 }, 1541 { 1542 point: model.SamplePair{10, 20}, 1543 expected: `[0.010,"20"]`, 1544 }, 1545 { 1546 point: model.SamplePair{100, 20}, 1547 expected: `[0.100,"20"]`, 1548 }, 1549 { 1550 point: model.SamplePair{1001, 20}, 1551 expected: `[1.001,"20"]`, 1552 }, 1553 { 1554 point: model.SamplePair{1010, 20}, 1555 expected: `[1.010,"20"]`, 1556 }, 1557 { 1558 point: model.SamplePair{1100, 20}, 1559 expected: `[1.100,"20"]`, 1560 }, 1561 { 1562 point: model.SamplePair{12345678123456555, 20}, 1563 expected: `[12345678123456.555,"20"]`, 1564 }, 1565 { 1566 point: model.SamplePair{-1, 20}, 1567 expected: `[-0.001,"20"]`, 1568 }, 1569 { 1570 point: model.SamplePair{0, model.SampleValue(math.NaN())}, 1571 expected: `[0,"NaN"]`, 1572 }, 1573 { 1574 point: model.SamplePair{0, model.SampleValue(math.Inf(1))}, 1575 expected: `[0,"+Inf"]`, 1576 }, 1577 { 1578 point: model.SamplePair{0, model.SampleValue(math.Inf(-1))}, 1579 expected: `[0,"-Inf"]`, 1580 }, 1581 { 1582 point: model.SamplePair{0, model.SampleValue(1.2345678e6)}, 1583 expected: `[0,"1234567.8"]`, 1584 }, 1585 { 1586 point: model.SamplePair{0, 1.2345678e-6}, 1587 expected: `[0,"0.0000012345678"]`, 1588 }, 1589 { 1590 point: model.SamplePair{0, 1.2345678e-67}, 1591 expected: `[0,"1.2345678e-67"]`, 1592 }, 1593 } 1594 1595 for _, test := range tests { 1596 t.Run(test.expected, func(t *testing.T) { 1597 b, err := json.Marshal(test.point) 1598 if err != nil { 1599 t.Fatal(err) 1600 } 1601 if string(b) != test.expected { 1602 t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b)) 1603 } 1604 1605 // To test Unmarshal we will Unmarshal then re-Marshal this way we 1606 // can do a string compare, otherwise Nan values don't show equivalence 1607 // properly. 1608 var sp model.SamplePair 1609 if err = json.Unmarshal(b, &sp); err != nil { 1610 t.Fatal(err) 1611 } 1612 1613 b, err = json.Marshal(sp) 1614 if err != nil { 1615 t.Fatal(err) 1616 } 1617 if string(b) != test.expected { 1618 t.Fatalf("Mismatch marshal expected=%s actual=%s", test.expected, string(b)) 1619 } 1620 }) 1621 } 1622} 1623 1624type httpTestClient struct { 1625 client http.Client 1626} 1627 1628func (c *httpTestClient) URL(ep string, args map[string]string) *url.URL { 1629 return nil 1630} 1631 1632func (c *httpTestClient) Do(ctx context.Context, req *http.Request) (*http.Response, []byte, error) { 1633 resp, err := c.client.Do(req) 1634 if err != nil { 1635 return nil, nil, err 1636 } 1637 1638 var body []byte 1639 done := make(chan struct{}) 1640 go func() { 1641 body, err = ioutil.ReadAll(resp.Body) 1642 close(done) 1643 }() 1644 1645 select { 1646 case <-ctx.Done(): 1647 <-done 1648 err = resp.Body.Close() 1649 if err == nil { 1650 err = ctx.Err() 1651 } 1652 case <-done: 1653 } 1654 1655 return resp, body, err 1656} 1657 1658func TestDoGetFallback(t *testing.T) { 1659 v := url.Values{"a": []string{"1", "2"}} 1660 1661 type testResponse struct { 1662 Values string 1663 Method string 1664 } 1665 1666 // Start a local HTTP server. 1667 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 1668 req.ParseForm() 1669 testResp, _ := json.Marshal(&testResponse{ 1670 Values: req.Form.Encode(), 1671 Method: req.Method, 1672 }) 1673 1674 apiResp := &apiResponse{ 1675 Data: testResp, 1676 } 1677 1678 body, _ := json.Marshal(apiResp) 1679 1680 if req.Method == http.MethodPost { 1681 if req.URL.Path == "/blockPost405" { 1682 http.Error(w, string(body), http.StatusMethodNotAllowed) 1683 return 1684 } 1685 } 1686 1687 if req.Method == http.MethodPost { 1688 if req.URL.Path == "/blockPost501" { 1689 http.Error(w, string(body), http.StatusNotImplemented) 1690 return 1691 } 1692 } 1693 1694 w.Write(body) 1695 })) 1696 // Close the server when test finishes. 1697 defer server.Close() 1698 1699 u, err := url.Parse(server.URL) 1700 if err != nil { 1701 t.Fatal(err) 1702 } 1703 client := &httpTestClient{client: *(server.Client())} 1704 api := &apiClientImpl{ 1705 client: client, 1706 } 1707 1708 // Do a post, and ensure that the post succeeds. 1709 _, b, _, err := api.DoGetFallback(context.TODO(), u, v) 1710 if err != nil { 1711 t.Fatalf("Error doing local request: %v", err) 1712 } 1713 resp := &testResponse{} 1714 if err := json.Unmarshal(b, resp); err != nil { 1715 t.Fatal(err) 1716 } 1717 if resp.Method != http.MethodPost { 1718 t.Fatalf("Mismatch method") 1719 } 1720 if resp.Values != v.Encode() { 1721 t.Fatalf("Mismatch in values") 1722 } 1723 1724 // Do a fallback to a get on 405. 1725 u.Path = "/blockPost405" 1726 _, b, _, err = api.DoGetFallback(context.TODO(), u, v) 1727 if err != nil { 1728 t.Fatalf("Error doing local request: %v", err) 1729 } 1730 if err := json.Unmarshal(b, resp); err != nil { 1731 t.Fatal(err) 1732 } 1733 if resp.Method != http.MethodGet { 1734 t.Fatalf("Mismatch method") 1735 } 1736 if resp.Values != v.Encode() { 1737 t.Fatalf("Mismatch in values") 1738 } 1739 1740 // Do a fallback to a get on 501. 1741 u.Path = "/blockPost501" 1742 _, b, _, err = api.DoGetFallback(context.TODO(), u, v) 1743 if err != nil { 1744 t.Fatalf("Error doing local request: %v", err) 1745 } 1746 if err := json.Unmarshal(b, resp); err != nil { 1747 t.Fatal(err) 1748 } 1749 if resp.Method != http.MethodGet { 1750 t.Fatalf("Mismatch method") 1751 } 1752 if resp.Values != v.Encode() { 1753 t.Fatalf("Mismatch in values") 1754 } 1755} 1756