1package cloudflare 2 3import ( 4 "context" 5 "crypto/md5" // for generating IDs 6 "encoding/hex" // for generating IDs 7 "encoding/json" 8 "fmt" 9 "net/http" 10 "net/url" 11 "strconv" 12 "testing" 13 "time" 14 15 "github.com/stretchr/testify/assert" 16) 17 18// mockID returns a hex string of length 32, suitable for all kinds of IDs 19// used in the Cloudflare API. 20func mockID(seed string) string { 21 arr := md5.Sum([]byte(seed)) 22 return hex.EncodeToString(arr[:]) 23} 24 25func mustParseTime(s string) time.Time { 26 t, err := time.Parse(time.RFC3339Nano, s) 27 if err != nil { 28 panic(err) 29 } 30 return t 31} 32 33func mockZone(i int) *Zone { 34 zoneName := fmt.Sprintf("%d.example.com", i) 35 ownerName := "Test Account" 36 37 return &Zone{ 38 ID: mockID(zoneName), 39 Name: zoneName, 40 DevMode: 0, 41 OriginalNS: []string{ 42 "linda.ns.cloudflare.com", 43 "merlin.ns.cloudflare.com", 44 }, 45 OriginalRegistrar: "cloudflare, inc. (id: 1910)", 46 OriginalDNSHost: "", 47 CreatedOn: mustParseTime("2021-07-28T05:06:20.736244Z"), 48 ModifiedOn: mustParseTime("2021-07-28T05:06:20.736244Z"), 49 NameServers: []string{ 50 "abby.ns.cloudflare.com", 51 "noel.ns.cloudflare.com", 52 }, 53 Owner: Owner{ 54 ID: mockID(ownerName), 55 Email: "", 56 Name: ownerName, 57 OwnerType: "organization", 58 }, 59 Permissions: []string{ 60 "#access:read", 61 "#analytics:read", 62 "#auditlogs:read", 63 "#billing:read", 64 "#dns_records:read", 65 "#lb:read", 66 "#legal:read", 67 "#logs:read", 68 "#member:read", 69 "#organization:read", 70 "#ssl:read", 71 "#stream:read", 72 "#subscription:read", 73 "#waf:read", 74 "#webhooks:read", 75 "#worker:read", 76 "#zone:read", 77 "#zone_settings:read", 78 }, 79 Plan: ZonePlan{ 80 ZonePlanCommon: ZonePlanCommon{ 81 ID: "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", 82 Name: "Free Website", 83 Currency: "USD", 84 }, 85 IsSubscribed: false, 86 CanSubscribe: false, 87 LegacyID: "free", 88 LegacyDiscount: false, 89 ExternallyManaged: false, 90 }, 91 PlanPending: ZonePlan{ 92 ZonePlanCommon: ZonePlanCommon{ 93 ID: "", 94 }, 95 IsSubscribed: false, 96 CanSubscribe: false, 97 LegacyID: "", 98 LegacyDiscount: false, 99 ExternallyManaged: false, 100 }, 101 Status: "active", 102 Paused: false, 103 Type: "full", 104 Host: struct { 105 Name string 106 Website string 107 }{ 108 Name: "", 109 Website: "", 110 }, 111 VanityNS: nil, 112 Betas: nil, 113 DeactReason: "", 114 Meta: ZoneMeta{ 115 PageRuleQuota: 3, 116 WildcardProxiable: false, 117 PhishingDetected: false, 118 }, 119 Account: Account{ 120 ID: mockID(ownerName), 121 Name: ownerName, 122 }, 123 VerificationKey: "", 124 } 125} 126 127func mockZonesResponse(total, page, start, count int) *ZonesResponse { 128 zones := make([]Zone, count) 129 for i := range zones { 130 zones[i] = *mockZone(start + i) 131 } 132 133 return &ZonesResponse{ 134 Result: zones, 135 ResultInfo: ResultInfo{ 136 Page: page, 137 PerPage: 50, 138 TotalPages: (total + 49) / 50, 139 Count: count, 140 Total: total, 141 }, 142 Response: Response{ 143 Success: true, 144 Errors: []ResponseInfo{}, 145 Messages: []ResponseInfo{}, 146 }, 147 } 148} 149 150func TestZoneAnalyticsDashboard(t *testing.T) { 151 setup() 152 defer teardown() 153 154 handler := func(w http.ResponseWriter, r *http.Request) { 155 assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) 156 assert.Equal(t, "2015-01-01T12:23:00Z", r.URL.Query().Get("since")) 157 assert.Equal(t, "2015-01-02T12:23:00Z", r.URL.Query().Get("until")) 158 assert.Equal(t, "true", r.URL.Query().Get("continuous")) 159 160 w.Header().Set("content-type", "application/json") 161 // JSON data from: https://api.cloudflare.com/#zone-analytics-properties 162 fmt.Fprintf(w, `{ 163 "success": true, 164 "errors": [], 165 "messages": [], 166 "result": { 167 "totals": { 168 "since": "2015-01-01T12:23:00Z", 169 "until": "2015-01-02T12:23:00Z", 170 "requests": { 171 "all": 1234085328, 172 "cached": 1234085328, 173 "uncached": 13876154, 174 "content_type": { 175 "css": 15343, 176 "html": 1234213, 177 "javascript": 318236, 178 "gif": 23178, 179 "jpeg": 1982048 180 }, 181 "country": { 182 "US": 4181364, 183 "AG": 37298, 184 "GI": 293846 185 }, 186 "ssl": { 187 "encrypted": 12978361, 188 "unencrypted": 781263 189 }, 190 "http_status": { 191 "200": 13496983, 192 "301": 283, 193 "400": 187936, 194 "402": 1828, 195 "404": 1293 196 } 197 }, 198 "bandwidth": { 199 "all": 213867451, 200 "cached": 113205063, 201 "uncached": 113205063, 202 "content_type": { 203 "css": 237421, 204 "html": 1231290, 205 "javascript": 123245, 206 "gif": 1234242, 207 "jpeg": 784278 208 }, 209 "country": { 210 "US": 123145433, 211 "AG": 2342483, 212 "GI": 984753 213 }, 214 "ssl": { 215 "encrypted": 37592942, 216 "unencrypted": 237654192 217 } 218 }, 219 "threats": { 220 "all": 23423873, 221 "country": { 222 "US": 123, 223 "CN": 523423, 224 "AU": 91 225 }, 226 "type": { 227 "user.ban.ip": 123, 228 "hot.ban.unknown": 5324, 229 "macro.chl.captchaErr": 1341, 230 "macro.chl.jschlErr": 5323 231 } 232 }, 233 "pageviews": { 234 "all": 5724723, 235 "search_engines": { 236 "googlebot": 35272, 237 "pingdom": 13435, 238 "bingbot": 5372, 239 "baidubot": 1345 240 } 241 }, 242 "uniques": { 243 "all": 12343 244 } 245 }, 246 "timeseries": [ 247 { 248 "since": "2015-01-01T12:23:00Z", 249 "until": "2015-01-02T12:23:00Z", 250 "requests": { 251 "all": 1234085328, 252 "cached": 1234085328, 253 "uncached": 13876154, 254 "content_type": { 255 "css": 15343, 256 "html": 1234213, 257 "javascript": 318236, 258 "gif": 23178, 259 "jpeg": 1982048 260 }, 261 "country": { 262 "US": 4181364, 263 "AG": 37298, 264 "GI": 293846 265 }, 266 "ssl": { 267 "encrypted": 12978361, 268 "unencrypted": 781263 269 }, 270 "http_status": { 271 "200": 13496983, 272 "301": 283, 273 "400": 187936, 274 "402": 1828, 275 "404": 1293 276 } 277 }, 278 "bandwidth": { 279 "all": 213867451, 280 "cached": 113205063, 281 "uncached": 113205063, 282 "content_type": { 283 "css": 237421, 284 "html": 1231290, 285 "javascript": 123245, 286 "gif": 1234242, 287 "jpeg": 784278 288 }, 289 "country": { 290 "US": 123145433, 291 "AG": 2342483, 292 "GI": 984753 293 }, 294 "ssl": { 295 "encrypted": 37592942, 296 "unencrypted": 237654192 297 } 298 }, 299 "threats": { 300 "all": 23423873, 301 "country": { 302 "US": 123, 303 "CN": 523423, 304 "AU": 91 305 }, 306 "type": { 307 "user.ban.ip": 123, 308 "hot.ban.unknown": 5324, 309 "macro.chl.captchaErr": 1341, 310 "macro.chl.jschlErr": 5323 311 } 312 }, 313 "pageviews": { 314 "all": 5724723, 315 "search_engines": { 316 "googlebot": 35272, 317 "pingdom": 13435, 318 "bingbot": 5372, 319 "baidubot": 1345 320 } 321 }, 322 "uniques": { 323 "all": 12343 324 } 325 } 326 ] 327 }, 328 "query": { 329 "since": "2015-01-01T12:23:00Z", 330 "until": "2015-01-02T12:23:00Z", 331 "time_delta": 60 332 } 333 }`) 334 } 335 336 mux.HandleFunc("/zones/foo/analytics/dashboard", handler) 337 338 since, _ := time.Parse(time.RFC3339, "2015-01-01T12:23:00Z") 339 until, _ := time.Parse(time.RFC3339, "2015-01-02T12:23:00Z") 340 data := ZoneAnalytics{ 341 Since: since, 342 Until: until, 343 Requests: struct { 344 All int `json:"all"` 345 Cached int `json:"cached"` 346 Uncached int `json:"uncached"` 347 ContentType map[string]int `json:"content_type"` 348 Country map[string]int `json:"country"` 349 SSL struct { 350 Encrypted int `json:"encrypted"` 351 Unencrypted int `json:"unencrypted"` 352 } `json:"ssl"` 353 HTTPStatus map[string]int `json:"http_status"` 354 }{ 355 All: 1234085328, 356 Cached: 1234085328, 357 Uncached: 13876154, 358 ContentType: map[string]int{ 359 "css": 15343, 360 "html": 1234213, 361 "javascript": 318236, 362 "gif": 23178, 363 "jpeg": 1982048, 364 }, 365 Country: map[string]int{ 366 "US": 4181364, 367 "AG": 37298, 368 "GI": 293846, 369 }, 370 SSL: struct { 371 Encrypted int `json:"encrypted"` 372 Unencrypted int `json:"unencrypted"` 373 }{ 374 Encrypted: 12978361, 375 Unencrypted: 781263, 376 }, 377 HTTPStatus: map[string]int{ 378 "200": 13496983, 379 "301": 283, 380 "400": 187936, 381 "402": 1828, 382 "404": 1293, 383 }, 384 }, 385 Bandwidth: struct { 386 All int `json:"all"` 387 Cached int `json:"cached"` 388 Uncached int `json:"uncached"` 389 ContentType map[string]int `json:"content_type"` 390 Country map[string]int `json:"country"` 391 SSL struct { 392 Encrypted int `json:"encrypted"` 393 Unencrypted int `json:"unencrypted"` 394 } `json:"ssl"` 395 }{ 396 All: 213867451, 397 Cached: 113205063, 398 Uncached: 113205063, 399 ContentType: map[string]int{ 400 "css": 237421, 401 "html": 1231290, 402 "javascript": 123245, 403 "gif": 1234242, 404 "jpeg": 784278, 405 }, 406 Country: map[string]int{ 407 "US": 123145433, 408 "AG": 2342483, 409 "GI": 984753, 410 }, 411 SSL: struct { 412 Encrypted int `json:"encrypted"` 413 Unencrypted int `json:"unencrypted"` 414 }{ 415 Encrypted: 37592942, 416 Unencrypted: 237654192, 417 }, 418 }, 419 Threats: struct { 420 All int `json:"all"` 421 Country map[string]int `json:"country"` 422 Type map[string]int `json:"type"` 423 }{ 424 All: 23423873, 425 Country: map[string]int{ 426 "US": 123, 427 "CN": 523423, 428 "AU": 91, 429 }, 430 Type: map[string]int{ 431 "user.ban.ip": 123, 432 "hot.ban.unknown": 5324, 433 "macro.chl.captchaErr": 1341, 434 "macro.chl.jschlErr": 5323, 435 }, 436 }, 437 Pageviews: struct { 438 All int `json:"all"` 439 SearchEngines map[string]int `json:"search_engines"` 440 }{ 441 All: 5724723, 442 SearchEngines: map[string]int{ 443 "googlebot": 35272, 444 "pingdom": 13435, 445 "bingbot": 5372, 446 "baidubot": 1345, 447 }, 448 }, 449 Uniques: struct { 450 All int `json:"all"` 451 }{ 452 All: 12343, 453 }, 454 } 455 want := ZoneAnalyticsData{ 456 Totals: data, 457 Timeseries: []ZoneAnalytics{data}, 458 } 459 460 continuous := true 461 d, err := client.ZoneAnalyticsDashboard(context.Background(), "foo", ZoneAnalyticsOptions{ 462 Since: &since, 463 Until: &until, 464 Continuous: &continuous, 465 }) 466 if assert.NoError(t, err) { 467 assert.Equal(t, want, d) 468 } 469 470 _, err = client.ZoneAnalyticsDashboard(context.Background(), "bar", ZoneAnalyticsOptions{}) 471 assert.Error(t, err) 472} 473 474func TestZoneAnalyticsByColocation(t *testing.T) { 475 setup() 476 defer teardown() 477 478 handler := func(w http.ResponseWriter, r *http.Request) { 479 assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) 480 assert.Equal(t, "2015-01-01T12:23:00Z", r.URL.Query().Get("since")) 481 assert.Equal(t, "2015-01-02T12:23:00Z", r.URL.Query().Get("until")) 482 assert.Equal(t, "true", r.URL.Query().Get("continuous")) 483 484 w.Header().Set("content-type", "application/json") 485 // JSON data from: https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations 486 fmt.Fprintf(w, `{ 487 "success": true, 488 "errors": [], 489 "messages": [], 490 "result": [ 491 { 492 "colo_id": "SFO", 493 "timeseries": [ 494 { 495 "since": "2015-01-01T12:23:00Z", 496 "until": "2015-01-02T12:23:00Z", 497 "requests": { 498 "all": 1234085328, 499 "cached": 1234085328, 500 "uncached": 13876154, 501 "content_type": { 502 "css": 15343, 503 "html": 1234213, 504 "javascript": 318236, 505 "gif": 23178, 506 "jpeg": 1982048 507 }, 508 "country": { 509 "US": 4181364, 510 "AG": 37298, 511 "GI": 293846 512 }, 513 "ssl": { 514 "encrypted": 12978361, 515 "unencrypted": 781263 516 }, 517 "http_status": { 518 "200": 13496983, 519 "301": 283, 520 "400": 187936, 521 "402": 1828, 522 "404": 1293 523 } 524 }, 525 "bandwidth": { 526 "all": 213867451, 527 "cached": 113205063, 528 "uncached": 113205063, 529 "content_type": { 530 "css": 237421, 531 "html": 1231290, 532 "javascript": 123245, 533 "gif": 1234242, 534 "jpeg": 784278 535 }, 536 "country": { 537 "US": 123145433, 538 "AG": 2342483, 539 "GI": 984753 540 }, 541 "ssl": { 542 "encrypted": 37592942, 543 "unencrypted": 237654192 544 } 545 }, 546 "threats": { 547 "all": 23423873, 548 "country": { 549 "US": 123, 550 "CN": 523423, 551 "AU": 91 552 }, 553 "type": { 554 "user.ban.ip": 123, 555 "hot.ban.unknown": 5324, 556 "macro.chl.captchaErr": 1341, 557 "macro.chl.jschlErr": 5323 558 } 559 }, 560 "pageviews": { 561 "all": 5724723, 562 "search_engines": { 563 "googlebot": 35272, 564 "pingdom": 13435, 565 "bingbot": 5372, 566 "baidubot": 1345 567 } 568 }, 569 "uniques": { 570 "all": 12343 571 } 572 } 573 ] 574 } 575 ], 576 "query": { 577 "since": "2015-01-01T12:23:00Z", 578 "until": "2015-01-02T12:23:00Z", 579 "time_delta": 60 580 } 581 }`) 582 } 583 584 mux.HandleFunc("/zones/foo/analytics/colos", handler) 585 586 since, _ := time.Parse(time.RFC3339, "2015-01-01T12:23:00Z") 587 until, _ := time.Parse(time.RFC3339, "2015-01-02T12:23:00Z") 588 data := ZoneAnalytics{ 589 Since: since, 590 Until: until, 591 Requests: struct { 592 All int `json:"all"` 593 Cached int `json:"cached"` 594 Uncached int `json:"uncached"` 595 ContentType map[string]int `json:"content_type"` 596 Country map[string]int `json:"country"` 597 SSL struct { 598 Encrypted int `json:"encrypted"` 599 Unencrypted int `json:"unencrypted"` 600 } `json:"ssl"` 601 HTTPStatus map[string]int `json:"http_status"` 602 }{ 603 All: 1234085328, 604 Cached: 1234085328, 605 Uncached: 13876154, 606 ContentType: map[string]int{ 607 "css": 15343, 608 "html": 1234213, 609 "javascript": 318236, 610 "gif": 23178, 611 "jpeg": 1982048, 612 }, 613 Country: map[string]int{ 614 "US": 4181364, 615 "AG": 37298, 616 "GI": 293846, 617 }, 618 SSL: struct { 619 Encrypted int `json:"encrypted"` 620 Unencrypted int `json:"unencrypted"` 621 }{ 622 Encrypted: 12978361, 623 Unencrypted: 781263, 624 }, 625 HTTPStatus: map[string]int{ 626 "200": 13496983, 627 "301": 283, 628 "400": 187936, 629 "402": 1828, 630 "404": 1293, 631 }, 632 }, 633 Bandwidth: struct { 634 All int `json:"all"` 635 Cached int `json:"cached"` 636 Uncached int `json:"uncached"` 637 ContentType map[string]int `json:"content_type"` 638 Country map[string]int `json:"country"` 639 SSL struct { 640 Encrypted int `json:"encrypted"` 641 Unencrypted int `json:"unencrypted"` 642 } `json:"ssl"` 643 }{ 644 All: 213867451, 645 Cached: 113205063, 646 Uncached: 113205063, 647 ContentType: map[string]int{ 648 "css": 237421, 649 "html": 1231290, 650 "javascript": 123245, 651 "gif": 1234242, 652 "jpeg": 784278, 653 }, 654 Country: map[string]int{ 655 "US": 123145433, 656 "AG": 2342483, 657 "GI": 984753, 658 }, 659 SSL: struct { 660 Encrypted int `json:"encrypted"` 661 Unencrypted int `json:"unencrypted"` 662 }{ 663 Encrypted: 37592942, 664 Unencrypted: 237654192, 665 }, 666 }, 667 Threats: struct { 668 All int `json:"all"` 669 Country map[string]int `json:"country"` 670 Type map[string]int `json:"type"` 671 }{ 672 All: 23423873, 673 Country: map[string]int{ 674 "US": 123, 675 "CN": 523423, 676 "AU": 91, 677 }, 678 Type: map[string]int{ 679 "user.ban.ip": 123, 680 "hot.ban.unknown": 5324, 681 "macro.chl.captchaErr": 1341, 682 "macro.chl.jschlErr": 5323, 683 }, 684 }, 685 Pageviews: struct { 686 All int `json:"all"` 687 SearchEngines map[string]int `json:"search_engines"` 688 }{ 689 All: 5724723, 690 SearchEngines: map[string]int{ 691 "googlebot": 35272, 692 "pingdom": 13435, 693 "bingbot": 5372, 694 "baidubot": 1345, 695 }, 696 }, 697 Uniques: struct { 698 All int `json:"all"` 699 }{ 700 All: 12343, 701 }, 702 } 703 want := []ZoneAnalyticsColocation{ 704 { 705 ColocationID: "SFO", 706 Timeseries: []ZoneAnalytics{data}, 707 }, 708 } 709 710 continuous := true 711 d, err := client.ZoneAnalyticsByColocation(context.Background(), "foo", ZoneAnalyticsOptions{ 712 Since: &since, 713 Until: &until, 714 Continuous: &continuous, 715 }) 716 if assert.NoError(t, err) { 717 assert.Equal(t, want, d) 718 } 719 720 _, err = client.ZoneAnalyticsDashboard(context.Background(), "bar", ZoneAnalyticsOptions{}) 721 assert.Error(t, err) 722} 723 724func TestWithPagination(t *testing.T) { 725 opt := reqOption{ 726 params: url.Values{}, 727 } 728 popts := PaginationOptions{ 729 Page: 45, 730 PerPage: 500, 731 } 732 of := WithPagination(popts) 733 of(&opt) 734 735 tests := []struct { 736 name string 737 expected string 738 }{ 739 {"page", "45"}, 740 {"per_page", "500"}, 741 } 742 743 for _, tt := range tests { 744 if got := opt.params.Get(tt.name); got != tt.expected { 745 t.Errorf("expected param %s to be %s, got %s", tt.name, tt.expected, got) 746 } 747 } 748} 749 750func TestZoneFilters(t *testing.T) { 751 opt := reqOption{ 752 params: url.Values{}, 753 } 754 of := WithZoneFilters("example.org", "", "") 755 of(&opt) 756 757 if got := opt.params.Get("name"); got != "example.org" { 758 t.Errorf("expected param %s to be %s, got %s", "name", "example.org", got) 759 } 760} 761 762var createdAndModifiedOn, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") 763var expectedFullZoneSetup = Zone{ 764 ID: "023e105f4ecef8ad9ca31a8372d0c353", 765 Name: "example.com", 766 DevMode: 7200, 767 OriginalNS: []string{ 768 "ns1.originaldnshost.com", 769 "ns2.originaldnshost.com", 770 }, 771 OriginalRegistrar: "GoDaddy", 772 OriginalDNSHost: "NameCheap", 773 CreatedOn: createdAndModifiedOn, 774 ModifiedOn: createdAndModifiedOn, 775 Owner: Owner{ 776 ID: "7c5dae5552338874e5053f2534d2767a", 777 Email: "user@example.com", 778 OwnerType: "user", 779 }, 780 Account: Account{ 781 ID: "01a7362d577a6c3019a474fd6f485823", 782 Name: "Demo Account", 783 }, 784 Permissions: []string{"#zone:read", "#zone:edit"}, 785 Plan: ZonePlan{ 786 ZonePlanCommon: ZonePlanCommon{ 787 ID: "e592fd9519420ba7405e1307bff33214", 788 Name: "Pro Plan", 789 Price: 20, 790 Currency: "USD", 791 Frequency: "monthly", 792 }, 793 LegacyID: "pro", 794 IsSubscribed: true, 795 CanSubscribe: true, 796 }, 797 PlanPending: ZonePlan{ 798 ZonePlanCommon: ZonePlanCommon{ 799 ID: "e592fd9519420ba7405e1307bff33214", 800 Name: "Pro Plan", 801 Price: 20, 802 Currency: "USD", 803 Frequency: "monthly", 804 }, 805 LegacyID: "pro", 806 IsSubscribed: true, 807 CanSubscribe: true, 808 }, 809 Status: "active", 810 Paused: false, 811 Type: "full", 812 NameServers: []string{"tony.ns.cloudflare.com", "woz.ns.cloudflare.com"}, 813} 814var expectedPartialZoneSetup = Zone{ 815 ID: "023e105f4ecef8ad9ca31a8372d0c353", 816 Name: "example.com", 817 DevMode: 7200, 818 OriginalNS: []string{ 819 "ns1.originaldnshost.com", 820 "ns2.originaldnshost.com", 821 }, 822 OriginalRegistrar: "GoDaddy", 823 OriginalDNSHost: "NameCheap", 824 CreatedOn: createdAndModifiedOn, 825 ModifiedOn: createdAndModifiedOn, 826 Owner: Owner{ 827 ID: "7c5dae5552338874e5053f2534d2767a", 828 Email: "user@example.com", 829 OwnerType: "user", 830 }, 831 Account: Account{ 832 ID: "01a7362d577a6c3019a474fd6f485823", 833 Name: "Demo Account", 834 }, 835 Permissions: []string{"#zone:read", "#zone:edit"}, 836 Plan: ZonePlan{ 837 ZonePlanCommon: ZonePlanCommon{ 838 ID: "e592fd9519420ba7405e1307bff33214", 839 Name: "Pro Plan", 840 Price: 20, 841 Currency: "USD", 842 Frequency: "monthly", 843 }, 844 LegacyID: "pro", 845 IsSubscribed: true, 846 CanSubscribe: true, 847 }, 848 PlanPending: ZonePlan{ 849 ZonePlanCommon: ZonePlanCommon{ 850 ID: "e592fd9519420ba7405e1307bff33214", 851 Name: "Pro Plan", 852 Price: 20, 853 Currency: "USD", 854 Frequency: "monthly", 855 }, 856 LegacyID: "pro", 857 IsSubscribed: true, 858 CanSubscribe: true, 859 }, 860 Status: "active", 861 Paused: false, 862 Type: "partial", 863 NameServers: []string{"tony.ns.cloudflare.com", "woz.ns.cloudflare.com"}, 864} 865 866func TestCreateZoneFullSetup(t *testing.T) { 867 setup() 868 defer teardown() 869 870 handler := func(w http.ResponseWriter, r *http.Request) { 871 assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) 872 w.Header().Set("content-type", "application/json") 873 fmt.Fprintf(w, `{ 874 "success": true, 875 "errors": [], 876 "messages": [], 877 "result": { 878 "id": "023e105f4ecef8ad9ca31a8372d0c353", 879 "name": "example.com", 880 "development_mode": 7200, 881 "original_name_servers": [ 882 "ns1.originaldnshost.com", 883 "ns2.originaldnshost.com" 884 ], 885 "original_registrar": "GoDaddy", 886 "original_dnshost": "NameCheap", 887 "created_on": "2014-01-01T05:20:00.12345Z", 888 "modified_on": "2014-01-01T05:20:00.12345Z", 889 "activated_on": "2014-01-02T00:01:00.12345Z", 890 "owner": { 891 "id": "7c5dae5552338874e5053f2534d2767a", 892 "email": "user@example.com", 893 "type": "user" 894 }, 895 "account": { 896 "id": "01a7362d577a6c3019a474fd6f485823", 897 "name": "Demo Account" 898 }, 899 "permissions": [ 900 "#zone:read", 901 "#zone:edit" 902 ], 903 "plan": { 904 "id": "e592fd9519420ba7405e1307bff33214", 905 "name": "Pro Plan", 906 "price": 20, 907 "currency": "USD", 908 "frequency": "monthly", 909 "legacy_id": "pro", 910 "is_subscribed": true, 911 "can_subscribe": true 912 }, 913 "plan_pending": { 914 "id": "e592fd9519420ba7405e1307bff33214", 915 "name": "Pro Plan", 916 "price": 20, 917 "currency": "USD", 918 "frequency": "monthly", 919 "legacy_id": "pro", 920 "is_subscribed": true, 921 "can_subscribe": true 922 }, 923 "status": "active", 924 "paused": false, 925 "type": "full", 926 "name_servers": [ 927 "tony.ns.cloudflare.com", 928 "woz.ns.cloudflare.com" 929 ] 930 } 931 } 932 `) 933 } 934 935 mux.HandleFunc("/zones", handler) 936 937 actual, err := client.CreateZone(context.Background(), "example.com", false, Account{ID: "01a7362d577a6c3019a474fd6f485823"}, "full") 938 939 if assert.NoError(t, err) { 940 assert.Equal(t, expectedFullZoneSetup, actual) 941 } 942} 943 944func TestCreateZonePartialSetup(t *testing.T) { 945 setup() 946 defer teardown() 947 948 handler := func(w http.ResponseWriter, r *http.Request) { 949 assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) 950 w.Header().Set("content-type", "application/json") 951 fmt.Fprintf(w, `{ 952 "success": true, 953 "errors": [], 954 "messages": [], 955 "result": { 956 "id": "023e105f4ecef8ad9ca31a8372d0c353", 957 "name": "example.com", 958 "development_mode": 7200, 959 "original_name_servers": [ 960 "ns1.originaldnshost.com", 961 "ns2.originaldnshost.com" 962 ], 963 "original_registrar": "GoDaddy", 964 "original_dnshost": "NameCheap", 965 "created_on": "2014-01-01T05:20:00.12345Z", 966 "modified_on": "2014-01-01T05:20:00.12345Z", 967 "activated_on": "2014-01-02T00:01:00.12345Z", 968 "owner": { 969 "id": "7c5dae5552338874e5053f2534d2767a", 970 "email": "user@example.com", 971 "type": "user" 972 }, 973 "account": { 974 "id": "01a7362d577a6c3019a474fd6f485823", 975 "name": "Demo Account" 976 }, 977 "permissions": [ 978 "#zone:read", 979 "#zone:edit" 980 ], 981 "plan": { 982 "id": "e592fd9519420ba7405e1307bff33214", 983 "name": "Pro Plan", 984 "price": 20, 985 "currency": "USD", 986 "frequency": "monthly", 987 "legacy_id": "pro", 988 "is_subscribed": true, 989 "can_subscribe": true 990 }, 991 "plan_pending": { 992 "id": "e592fd9519420ba7405e1307bff33214", 993 "name": "Pro Plan", 994 "price": 20, 995 "currency": "USD", 996 "frequency": "monthly", 997 "legacy_id": "pro", 998 "is_subscribed": true, 999 "can_subscribe": true 1000 }, 1001 "status": "active", 1002 "paused": false, 1003 "type": "partial", 1004 "name_servers": [ 1005 "tony.ns.cloudflare.com", 1006 "woz.ns.cloudflare.com" 1007 ] 1008 } 1009 } 1010 `) 1011 } 1012 1013 mux.HandleFunc("/zones", handler) 1014 1015 actual, err := client.CreateZone(context.Background(), "example.com", false, Account{ID: "01a7362d577a6c3019a474fd6f485823"}, "partial") 1016 1017 if assert.NoError(t, err) { 1018 assert.Equal(t, expectedPartialZoneSetup, actual) 1019 } 1020} 1021 1022func TestFallbackOrigin_FallbackOrigin(t *testing.T) { 1023 setup() 1024 defer teardown() 1025 1026 mux.HandleFunc("/zones/foo/fallback_origin", func(w http.ResponseWriter, r *http.Request) { 1027 assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) 1028 1029 w.Header().Set("content-type", "application/json") 1030 fmt.Fprintf(w, `{ 1031"success": true, 1032"errors": [], 1033"messages": [], 1034"result": { 1035 "id": "fallback_origin", 1036 "value": "app.example.com", 1037 "editable": true 1038 } 1039}`) 1040 }) 1041 1042 fallbackOrigin, err := client.FallbackOrigin(context.Background(), "foo") 1043 1044 want := FallbackOrigin{ 1045 ID: "fallback_origin", 1046 Value: "app.example.com", 1047 } 1048 1049 if assert.NoError(t, err) { 1050 assert.Equal(t, want, fallbackOrigin) 1051 } 1052} 1053 1054func TestFallbackOrigin_UpdateFallbackOrigin(t *testing.T) { 1055 setup() 1056 defer teardown() 1057 1058 mux.HandleFunc("/zones/foo/fallback_origin", func(w http.ResponseWriter, r *http.Request) { 1059 assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) 1060 1061 w.Header().Set("content-type", "application/json") 1062 w.WriteHeader(http.StatusCreated) 1063 fmt.Fprintf(w, ` 1064{ 1065 "success": true, 1066 "errors": [], 1067 "messages": [], 1068 "result": { 1069 "id": "fallback_origin", 1070 "value": "app.example.com", 1071 "editable": true 1072 } 1073}`) 1074 }) 1075 1076 response, err := client.UpdateFallbackOrigin(context.Background(), "foo", FallbackOrigin{Value: "app.example.com"}) 1077 1078 want := &FallbackOriginResponse{ 1079 Result: FallbackOrigin{ 1080 ID: "fallback_origin", 1081 Value: "app.example.com", 1082 }, 1083 Response: Response{Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}}, 1084 } 1085 1086 if assert.NoError(t, err) { 1087 assert.Equal(t, want, response) 1088 } 1089} 1090 1091func Test_normalizeZoneName(t *testing.T) { 1092 tests := []struct { 1093 name string 1094 zone string 1095 expected string 1096 }{ 1097 { 1098 name: "unicode stays unicode", 1099 zone: "ünì¢øðe.tld", 1100 expected: "ünì¢øðe.tld", 1101 }, { 1102 name: "valid punycode is normalized to unicode", 1103 zone: "xn--ne-7ca90ava1cya.tld", 1104 expected: "ünì¢øðe.tld", 1105 }, { 1106 name: "valid punycode in second label", 1107 zone: "example.xn--j6w193g", 1108 expected: "example.香港", 1109 }, { 1110 name: "invalid punycode is returned without change", 1111 zone: "xn-invalid.xn-invalid-tld", 1112 expected: "xn-invalid.xn-invalid-tld", 1113 }, 1114 } 1115 for _, tt := range tests { 1116 t.Run(tt.name, func(t *testing.T) { 1117 actual := normalizeZoneName(tt.zone) 1118 assert.Equal(t, tt.expected, actual) 1119 }) 1120 } 1121} 1122 1123func TestZonePartialHasVerificationKey(t *testing.T) { 1124 setup() 1125 defer teardown() 1126 1127 handler := func(w http.ResponseWriter, r *http.Request) { 1128 assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) 1129 1130 w.Header().Set("content-type", "application/json") 1131 // JSON data from: https://api.cloudflare.com/#zone-zone-details (plus an undocumented field verification_key from curl to API) 1132 fmt.Fprintf(w, `{ 1133 "result": { 1134 "id": "foo", 1135 "name": "bar", 1136 "status": "active", 1137 "paused": false, 1138 "type": "partial", 1139 "development_mode": 0, 1140 "verification_key": "foo-bar", 1141 "original_name_servers": ["a","b","c","d"], 1142 "original_registrar": null, 1143 "original_dnshost": null, 1144 "modified_on": "2019-09-04T15:11:43.409805Z", 1145 "created_on": "2018-12-06T14:33:38.410126Z", 1146 "activated_on": "2018-12-06T14:34:39.274528Z", 1147 "meta": { 1148 "step": 4, 1149 "wildcard_proxiable": true, 1150 "custom_certificate_quota": 1, 1151 "page_rule_quota": 100, 1152 "phishing_detected": false, 1153 "multiple_railguns_allowed": false 1154 }, 1155 "owner": { 1156 "id": "bbbbbbbbbbbbbbbbbbbbbbbb", 1157 "type": "organization", 1158 "name": "OrgName" 1159 }, 1160 "account": { 1161 "id": "aaaaaaaaaaaaaaaaaaaaaaaa", 1162 "name": "AccountName" 1163 }, 1164 "permissions": [ 1165 "#access:edit", 1166 "#access:read", 1167 "#analytics:read", 1168 "#app:edit", 1169 "#auditlogs:read", 1170 "#billing:read", 1171 "#cache_purge:edit", 1172 "#dns_records:edit", 1173 "#dns_records:read", 1174 "#lb:edit", 1175 "#lb:read", 1176 "#legal:read", 1177 "#logs:edit", 1178 "#logs:read", 1179 "#member:read", 1180 "#organization:edit", 1181 "#organization:read", 1182 "#ssl:edit", 1183 "#ssl:read", 1184 "#stream:edit", 1185 "#stream:read", 1186 "#subscription:edit", 1187 "#subscription:read", 1188 "#waf:edit", 1189 "#waf:read", 1190 "#webhooks:edit", 1191 "#webhooks:read", 1192 "#worker:edit", 1193 "#worker:read", 1194 "#zone:edit", 1195 "#zone:read", 1196 "#zone_settings:edit", 1197 "#zone_settings:read" 1198 ], 1199 "plan": { 1200 "id": "94f3b7b768b0458b56d2cac4fe5ec0f9", 1201 "name": "Enterprise Website", 1202 "price": 0, 1203 "currency": "USD", 1204 "frequency": "monthly", 1205 "is_subscribed": true, 1206 "can_subscribe": true, 1207 "legacy_id": "enterprise", 1208 "legacy_discount": false, 1209 "externally_managed": true 1210 } 1211 }, 1212 "success": true, 1213 "errors": [], 1214 "messages": [] 1215}`) 1216 } 1217 1218 mux.HandleFunc("/zones/foo", handler) 1219 1220 z, err := client.ZoneDetails(context.Background(), "foo") 1221 if assert.NoError(t, err) { 1222 assert.NotEmpty(t, z.VerificationKey) 1223 assert.Equal(t, z.VerificationKey, "foo-bar") 1224 } 1225} 1226 1227func TestZoneDNSSECSetting(t *testing.T) { 1228 setup() 1229 defer teardown() 1230 1231 handler := func(w http.ResponseWriter, r *http.Request) { 1232 assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) 1233 1234 w.Header().Set("content-type", "application/json") 1235 // JSON data from: https://api.cloudflare.com/#dnssec-properties 1236 fmt.Fprintf(w, `{ 1237 "result": { 1238 "status": "active", 1239 "flags": 257, 1240 "algorithm": "13", 1241 "key_type": "ECDSAP256SHA256", 1242 "digest_type": "2", 1243 "digest_algorithm": "SHA256", 1244 "digest": "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", 1245 "ds": "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", 1246 "key_tag": 42, 1247 "public_key": "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==", 1248 "modified_on": "2014-01-01T05:20:00Z" 1249 } 1250 }`) 1251 } 1252 1253 mux.HandleFunc("/zones/foo/dnssec", handler) 1254 1255 z, err := client.ZoneDNSSECSetting(context.Background(), "foo") 1256 if assert.NoError(t, err) { 1257 assert.Equal(t, z.Status, "active") 1258 assert.Equal(t, z.Flags, 257) 1259 assert.Equal(t, z.Algorithm, "13") 1260 assert.Equal(t, z.KeyType, "ECDSAP256SHA256") 1261 assert.Equal(t, z.DigestType, "2") 1262 assert.Equal(t, z.DigestAlgorithm, "SHA256") 1263 assert.Equal(t, z.Digest, "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") 1264 assert.Equal(t, z.DS, "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") 1265 assert.Equal(t, z.KeyTag, 42) 1266 assert.Equal(t, z.PublicKey, "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==") 1267 time, _ := time.Parse("2006-01-02T15:04:05Z", "2014-01-01T05:20:00Z") 1268 assert.Equal(t, z.ModifiedOn, time) 1269 } 1270} 1271 1272func TestDeleteZoneDNSSEC(t *testing.T) { 1273 setup() 1274 defer teardown() 1275 1276 handler := func(w http.ResponseWriter, r *http.Request) { 1277 assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method) 1278 1279 w.Header().Set("content-type", "application/json") 1280 // JSON data from: https://api.cloudflare.com/#dnssec-properties 1281 fmt.Fprintf(w, `{ 1282 "result": "foo" 1283 }`) 1284 } 1285 1286 mux.HandleFunc("/zones/foo/dnssec", handler) 1287 1288 z, err := client.DeleteZoneDNSSEC(context.Background(), "foo") 1289 if assert.NoError(t, err) { 1290 assert.Equal(t, z, "foo") 1291 } 1292} 1293 1294func TestUpdateZoneDNSSEC(t *testing.T) { 1295 setup() 1296 defer teardown() 1297 1298 handler := func(w http.ResponseWriter, r *http.Request) { 1299 assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) 1300 1301 w.Header().Set("content-type", "application/json") 1302 // JSON data from: https://api.cloudflare.com/#dnssec-properties 1303 fmt.Fprintf(w, `{ 1304 "result": { 1305 "status": "active", 1306 "flags": 257, 1307 "algorithm": "13", 1308 "key_type": "ECDSAP256SHA256", 1309 "digest_type": "2", 1310 "digest_algorithm": "SHA256", 1311 "digest": "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", 1312 "ds": "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", 1313 "key_tag": 42, 1314 "public_key": "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==", 1315 "modified_on": "2014-01-01T05:20:00Z" 1316 } 1317 }`) 1318 } 1319 1320 mux.HandleFunc("/zones/foo/dnssec", handler) 1321 1322 z, err := client.UpdateZoneDNSSEC(context.Background(), "foo", ZoneDNSSECUpdateOptions{ 1323 Status: "active", 1324 }) 1325 if assert.NoError(t, err) { 1326 assert.Equal(t, z.Status, "active") 1327 assert.Equal(t, z.Flags, 257) 1328 assert.Equal(t, z.Algorithm, "13") 1329 assert.Equal(t, z.KeyType, "ECDSAP256SHA256") 1330 assert.Equal(t, z.DigestType, "2") 1331 assert.Equal(t, z.DigestAlgorithm, "SHA256") 1332 assert.Equal(t, z.Digest, "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") 1333 assert.Equal(t, z.DS, "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") 1334 assert.Equal(t, z.KeyTag, 42) 1335 assert.Equal(t, z.PublicKey, "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==") 1336 time, _ := time.Parse("2006-01-02T15:04:05Z", "2014-01-01T05:20:00Z") 1337 assert.Equal(t, z.ModifiedOn, time) 1338 } 1339} 1340 1341func parsePage(t *testing.T, total int, s string) (int, bool) { 1342 if s == "" { 1343 return 1, true 1344 } 1345 1346 page, err := strconv.Atoi(s) 1347 if !assert.NoError(t, err) { 1348 return 0, false 1349 } 1350 1351 if !assert.LessOrEqual(t, page, total) || !assert.GreaterOrEqual(t, page, 1) { 1352 return 0, false 1353 } 1354 1355 return page, true 1356} 1357 1358func TestListZones(t *testing.T) { 1359 setup() 1360 defer teardown() 1361 1362 const ( 1363 total = 392 1364 totalPage = (total + 49) / 50 1365 ) 1366 1367 handler := func(w http.ResponseWriter, r *http.Request) { 1368 switch { 1369 case !assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method): 1370 return 1371 case !assert.Equal(t, "50", r.URL.Query().Get("per_page")): 1372 return 1373 } 1374 1375 page, ok := parsePage(t, totalPage, r.URL.Query().Get("page")) 1376 if !ok { 1377 return 1378 } 1379 1380 start := (page - 1) * 50 1381 1382 count := 50 1383 if page == totalPage { 1384 count = total - start 1385 } 1386 1387 res, err := json.Marshal(mockZonesResponse(total, page, start, count)) 1388 if !assert.NoError(t, err) { 1389 return 1390 } 1391 1392 w.Header().Set("content-type", "application/json") 1393 1394 if _, err = w.Write(res); assert.NoError(t, err) { 1395 return 1396 } 1397 } 1398 1399 mux.HandleFunc("/zones", handler) 1400 1401 zones, err := client.ListZones(context.Background()) 1402 if !assert.NoError(t, err) || !assert.Equal(t, total, len(zones)) { 1403 return 1404 } 1405 1406 for i, zone := range zones { 1407 assert.Equal(t, *mockZone(i), zone) 1408 } 1409} 1410 1411func TestListZonesFailingPages(t *testing.T) { 1412 setup() 1413 defer teardown() 1414 1415 const ( 1416 total = 1489 1417 totalPage = (total + 49) / 50 1418 ) 1419 1420 // the pages to reject 1421 isReject := func(i int) bool { return i == 4 || i == 7 } 1422 1423 handler := func(w http.ResponseWriter, r *http.Request) { 1424 switch { 1425 case !assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method): 1426 return 1427 case !assert.Equal(t, "50", r.URL.Query().Get("per_page")): 1428 return 1429 } 1430 1431 page, ok := parsePage(t, totalPage, r.URL.Query().Get("page")) 1432 switch { 1433 case !ok: 1434 return 1435 case isReject(page): 1436 return 1437 } 1438 1439 start := (page - 1) * 50 1440 1441 count := 50 1442 if page == totalPage { 1443 count = total - start 1444 } 1445 1446 res, err := json.Marshal(mockZonesResponse(total, page, start, count)) 1447 if !assert.NoError(t, err) { 1448 return 1449 } 1450 1451 w.Header().Set("content-type", "application/json") 1452 1453 if _, err = w.Write(res); assert.NoError(t, err) { 1454 return 1455 } 1456 } 1457 1458 mux.HandleFunc("/zones", handler) 1459 1460 _, err := client.ListZones(context.Background()) 1461 assert.Error(t, err) 1462} 1463 1464func TestListZonesContextManualPagination1(t *testing.T) { 1465 _, err := client.ListZonesContext(context.Background(), WithPagination(PaginationOptions{Page: 2})) 1466 assert.EqualError(t, err, errManualPagination) 1467} 1468 1469func TestListZonesContextManualPagination2(t *testing.T) { 1470 _, err := client.ListZonesContext(context.Background(), WithPagination(PaginationOptions{PerPage: 30})) 1471 assert.EqualError(t, err, errManualPagination) 1472} 1473