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