package cloudflare import ( "context" "crypto/md5" //nolint:gosec "encoding/hex" // for generating IDs "fmt" "net/http" "net/url" "strconv" "testing" "time" "github.com/goccy/go-json" "github.com/stretchr/testify/assert" ) // mockID returns a hex string of length 32, suitable for all kinds of IDs // used in the Cloudflare API. func mockID(seed string) string { arr := md5.Sum([]byte(seed)) //nolint:gosec return hex.EncodeToString(arr[:]) } func mustParseTime(s string) time.Time { t, err := time.Parse(time.RFC3339Nano, s) if err != nil { panic(err) } return t } func mockZone(i int) *Zone { zoneName := fmt.Sprintf("%d.example.com", i) ownerName := "Test Account" return &Zone{ ID: mockID(zoneName), Name: zoneName, DevMode: 0, OriginalNS: []string{ "linda.ns.cloudflare.com", "merlin.ns.cloudflare.com", }, OriginalRegistrar: "cloudflare, inc. (id: 1910)", OriginalDNSHost: "", CreatedOn: mustParseTime("2021-07-28T05:06:20.736244Z"), ModifiedOn: mustParseTime("2021-07-28T05:06:20.736244Z"), NameServers: []string{ "abby.ns.cloudflare.com", "noel.ns.cloudflare.com", }, Owner: Owner{ ID: mockID(ownerName), Email: "", Name: ownerName, OwnerType: "organization", }, Permissions: []string{ "#access:read", "#analytics:read", "#auditlogs:read", "#billing:read", "#dns_records:read", "#lb:read", "#legal:read", "#logs:read", "#member:read", "#organization:read", "#ssl:read", "#stream:read", "#subscription:read", "#waf:read", "#webhooks:read", "#worker:read", "#zone:read", "#zone_settings:read", }, Plan: ZonePlan{ ZonePlanCommon: ZonePlanCommon{ ID: "0feeeeeeeeeeeeeeeeeeeeeeeeeeeeee", Name: "Free Website", Currency: "USD", }, IsSubscribed: false, CanSubscribe: false, LegacyID: "free", LegacyDiscount: false, ExternallyManaged: false, }, PlanPending: ZonePlan{ ZonePlanCommon: ZonePlanCommon{ ID: "", }, IsSubscribed: false, CanSubscribe: false, LegacyID: "", LegacyDiscount: false, ExternallyManaged: false, }, Status: "active", Paused: false, Type: "full", Host: struct { Name string Website string }{ Name: "", Website: "", }, VanityNS: nil, Betas: nil, DeactReason: "", Meta: ZoneMeta{ PageRuleQuota: 3, WildcardProxiable: false, PhishingDetected: false, }, Account: Account{ ID: mockID(ownerName), Name: ownerName, }, VerificationKey: "", } } func mockZonesResponse(total, page, start, count int) *ZonesResponse { zones := make([]Zone, count) for i := range zones { zones[i] = *mockZone(start + i) } return &ZonesResponse{ Result: zones, ResultInfo: ResultInfo{ Page: page, PerPage: 50, TotalPages: (total + 49) / 50, Count: count, Total: total, }, Response: Response{ Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}, }, } } func TestZoneAnalyticsDashboard(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) assert.Equal(t, "2015-01-01T12:23:00Z", r.URL.Query().Get("since")) assert.Equal(t, "2015-01-02T12:23:00Z", r.URL.Query().Get("until")) assert.Equal(t, "true", r.URL.Query().Get("continuous")) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#zone-analytics-properties fmt.Fprintf(w, `{ "success": true, "errors": [], "messages": [], "result": { "totals": { "since": "2015-01-01T12:23:00Z", "until": "2015-01-02T12:23:00Z", "requests": { "all": 1234085328, "cached": 1234085328, "uncached": 13876154, "content_type": { "css": 15343, "html": 1234213, "javascript": 318236, "gif": 23178, "jpeg": 1982048 }, "country": { "US": 4181364, "AG": 37298, "GI": 293846 }, "ssl": { "encrypted": 12978361, "unencrypted": 781263 }, "http_status": { "200": 13496983, "301": 283, "400": 187936, "402": 1828, "404": 1293 } }, "bandwidth": { "all": 213867451, "cached": 113205063, "uncached": 113205063, "content_type": { "css": 237421, "html": 1231290, "javascript": 123245, "gif": 1234242, "jpeg": 784278 }, "country": { "US": 123145433, "AG": 2342483, "GI": 984753 }, "ssl": { "encrypted": 37592942, "unencrypted": 237654192 } }, "threats": { "all": 23423873, "country": { "US": 123, "CN": 523423, "AU": 91 }, "type": { "user.ban.ip": 123, "hot.ban.unknown": 5324, "macro.chl.captchaErr": 1341, "macro.chl.jschlErr": 5323 } }, "pageviews": { "all": 5724723, "search_engines": { "googlebot": 35272, "pingdom": 13435, "bingbot": 5372, "baidubot": 1345 } }, "uniques": { "all": 12343 } }, "timeseries": [ { "since": "2015-01-01T12:23:00Z", "until": "2015-01-02T12:23:00Z", "requests": { "all": 1234085328, "cached": 1234085328, "uncached": 13876154, "content_type": { "css": 15343, "html": 1234213, "javascript": 318236, "gif": 23178, "jpeg": 1982048 }, "country": { "US": 4181364, "AG": 37298, "GI": 293846 }, "ssl": { "encrypted": 12978361, "unencrypted": 781263 }, "http_status": { "200": 13496983, "301": 283, "400": 187936, "402": 1828, "404": 1293 } }, "bandwidth": { "all": 213867451, "cached": 113205063, "uncached": 113205063, "content_type": { "css": 237421, "html": 1231290, "javascript": 123245, "gif": 1234242, "jpeg": 784278 }, "country": { "US": 123145433, "AG": 2342483, "GI": 984753 }, "ssl": { "encrypted": 37592942, "unencrypted": 237654192 } }, "threats": { "all": 23423873, "country": { "US": 123, "CN": 523423, "AU": 91 }, "type": { "user.ban.ip": 123, "hot.ban.unknown": 5324, "macro.chl.captchaErr": 1341, "macro.chl.jschlErr": 5323 } }, "pageviews": { "all": 5724723, "search_engines": { "googlebot": 35272, "pingdom": 13435, "bingbot": 5372, "baidubot": 1345 } }, "uniques": { "all": 12343 } } ] }, "query": { "since": "2015-01-01T12:23:00Z", "until": "2015-01-02T12:23:00Z", "time_delta": 60 } }`) } mux.HandleFunc("/zones/foo/analytics/dashboard", handler) since, _ := time.Parse(time.RFC3339, "2015-01-01T12:23:00Z") until, _ := time.Parse(time.RFC3339, "2015-01-02T12:23:00Z") data := ZoneAnalytics{ Since: since, Until: until, Requests: struct { All int `json:"all"` Cached int `json:"cached"` Uncached int `json:"uncached"` ContentType map[string]int `json:"content_type"` Country map[string]int `json:"country"` SSL struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` } `json:"ssl"` HTTPStatus map[string]int `json:"http_status"` }{ All: 1234085328, Cached: 1234085328, Uncached: 13876154, ContentType: map[string]int{ "css": 15343, "html": 1234213, "javascript": 318236, "gif": 23178, "jpeg": 1982048, }, Country: map[string]int{ "US": 4181364, "AG": 37298, "GI": 293846, }, SSL: struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` }{ Encrypted: 12978361, Unencrypted: 781263, }, HTTPStatus: map[string]int{ "200": 13496983, "301": 283, "400": 187936, "402": 1828, "404": 1293, }, }, Bandwidth: struct { All int `json:"all"` Cached int `json:"cached"` Uncached int `json:"uncached"` ContentType map[string]int `json:"content_type"` Country map[string]int `json:"country"` SSL struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` } `json:"ssl"` }{ All: 213867451, Cached: 113205063, Uncached: 113205063, ContentType: map[string]int{ "css": 237421, "html": 1231290, "javascript": 123245, "gif": 1234242, "jpeg": 784278, }, Country: map[string]int{ "US": 123145433, "AG": 2342483, "GI": 984753, }, SSL: struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` }{ Encrypted: 37592942, Unencrypted: 237654192, }, }, Threats: struct { All int `json:"all"` Country map[string]int `json:"country"` Type map[string]int `json:"type"` }{ All: 23423873, Country: map[string]int{ "US": 123, "CN": 523423, "AU": 91, }, Type: map[string]int{ "user.ban.ip": 123, "hot.ban.unknown": 5324, "macro.chl.captchaErr": 1341, "macro.chl.jschlErr": 5323, }, }, Pageviews: struct { All int `json:"all"` SearchEngines map[string]int `json:"search_engines"` }{ All: 5724723, SearchEngines: map[string]int{ "googlebot": 35272, "pingdom": 13435, "bingbot": 5372, "baidubot": 1345, }, }, Uniques: struct { All int `json:"all"` }{ All: 12343, }, } want := ZoneAnalyticsData{ Totals: data, Timeseries: []ZoneAnalytics{data}, } continuous := true d, err := client.ZoneAnalyticsDashboard(context.Background(), "foo", ZoneAnalyticsOptions{ Since: &since, Until: &until, Continuous: &continuous, }) if assert.NoError(t, err) { assert.Equal(t, want, d) } _, err = client.ZoneAnalyticsDashboard(context.Background(), "bar", ZoneAnalyticsOptions{}) assert.Error(t, err) } func TestZoneAnalyticsByColocation(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) assert.Equal(t, "2015-01-01T12:23:00Z", r.URL.Query().Get("since")) assert.Equal(t, "2015-01-02T12:23:00Z", r.URL.Query().Get("until")) assert.Equal(t, "true", r.URL.Query().Get("continuous")) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#zone-analytics-analytics-by-co-locations fmt.Fprintf(w, `{ "success": true, "errors": [], "messages": [], "result": [ { "colo_id": "SFO", "timeseries": [ { "since": "2015-01-01T12:23:00Z", "until": "2015-01-02T12:23:00Z", "requests": { "all": 1234085328, "cached": 1234085328, "uncached": 13876154, "content_type": { "css": 15343, "html": 1234213, "javascript": 318236, "gif": 23178, "jpeg": 1982048 }, "country": { "US": 4181364, "AG": 37298, "GI": 293846 }, "ssl": { "encrypted": 12978361, "unencrypted": 781263 }, "http_status": { "200": 13496983, "301": 283, "400": 187936, "402": 1828, "404": 1293 } }, "bandwidth": { "all": 213867451, "cached": 113205063, "uncached": 113205063, "content_type": { "css": 237421, "html": 1231290, "javascript": 123245, "gif": 1234242, "jpeg": 784278 }, "country": { "US": 123145433, "AG": 2342483, "GI": 984753 }, "ssl": { "encrypted": 37592942, "unencrypted": 237654192 } }, "threats": { "all": 23423873, "country": { "US": 123, "CN": 523423, "AU": 91 }, "type": { "user.ban.ip": 123, "hot.ban.unknown": 5324, "macro.chl.captchaErr": 1341, "macro.chl.jschlErr": 5323 } }, "pageviews": { "all": 5724723, "search_engines": { "googlebot": 35272, "pingdom": 13435, "bingbot": 5372, "baidubot": 1345 } }, "uniques": { "all": 12343 } } ] } ], "query": { "since": "2015-01-01T12:23:00Z", "until": "2015-01-02T12:23:00Z", "time_delta": 60 } }`) } mux.HandleFunc("/zones/foo/analytics/colos", handler) since, _ := time.Parse(time.RFC3339, "2015-01-01T12:23:00Z") until, _ := time.Parse(time.RFC3339, "2015-01-02T12:23:00Z") data := ZoneAnalytics{ Since: since, Until: until, Requests: struct { All int `json:"all"` Cached int `json:"cached"` Uncached int `json:"uncached"` ContentType map[string]int `json:"content_type"` Country map[string]int `json:"country"` SSL struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` } `json:"ssl"` HTTPStatus map[string]int `json:"http_status"` }{ All: 1234085328, Cached: 1234085328, Uncached: 13876154, ContentType: map[string]int{ "css": 15343, "html": 1234213, "javascript": 318236, "gif": 23178, "jpeg": 1982048, }, Country: map[string]int{ "US": 4181364, "AG": 37298, "GI": 293846, }, SSL: struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` }{ Encrypted: 12978361, Unencrypted: 781263, }, HTTPStatus: map[string]int{ "200": 13496983, "301": 283, "400": 187936, "402": 1828, "404": 1293, }, }, Bandwidth: struct { All int `json:"all"` Cached int `json:"cached"` Uncached int `json:"uncached"` ContentType map[string]int `json:"content_type"` Country map[string]int `json:"country"` SSL struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` } `json:"ssl"` }{ All: 213867451, Cached: 113205063, Uncached: 113205063, ContentType: map[string]int{ "css": 237421, "html": 1231290, "javascript": 123245, "gif": 1234242, "jpeg": 784278, }, Country: map[string]int{ "US": 123145433, "AG": 2342483, "GI": 984753, }, SSL: struct { Encrypted int `json:"encrypted"` Unencrypted int `json:"unencrypted"` }{ Encrypted: 37592942, Unencrypted: 237654192, }, }, Threats: struct { All int `json:"all"` Country map[string]int `json:"country"` Type map[string]int `json:"type"` }{ All: 23423873, Country: map[string]int{ "US": 123, "CN": 523423, "AU": 91, }, Type: map[string]int{ "user.ban.ip": 123, "hot.ban.unknown": 5324, "macro.chl.captchaErr": 1341, "macro.chl.jschlErr": 5323, }, }, Pageviews: struct { All int `json:"all"` SearchEngines map[string]int `json:"search_engines"` }{ All: 5724723, SearchEngines: map[string]int{ "googlebot": 35272, "pingdom": 13435, "bingbot": 5372, "baidubot": 1345, }, }, Uniques: struct { All int `json:"all"` }{ All: 12343, }, } want := []ZoneAnalyticsColocation{ { ColocationID: "SFO", Timeseries: []ZoneAnalytics{data}, }, } continuous := true d, err := client.ZoneAnalyticsByColocation(context.Background(), "foo", ZoneAnalyticsOptions{ Since: &since, Until: &until, Continuous: &continuous, }) if assert.NoError(t, err) { assert.Equal(t, want, d) } _, err = client.ZoneAnalyticsDashboard(context.Background(), "bar", ZoneAnalyticsOptions{}) assert.Error(t, err) } func TestWithPagination(t *testing.T) { opt := reqOption{ params: url.Values{}, } popts := PaginationOptions{ Page: 45, PerPage: 500, } of := WithPagination(popts) of(&opt) tests := []struct { name string expected string }{ {"page", "45"}, {"per_page", "500"}, } for _, tt := range tests { if got := opt.params.Get(tt.name); got != tt.expected { t.Errorf("expected param %s to be %s, got %s", tt.name, tt.expected, got) } } } func TestZoneFilters(t *testing.T) { opt := reqOption{ params: url.Values{}, } of := WithZoneFilters("example.org", "", "") of(&opt) if got := opt.params.Get("name"); got != "example.org" { t.Errorf("expected param %s to be %s, got %s", "name", "example.org", got) } } var createdAndModifiedOn, _ = time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z") var expectedFullZoneSetup = Zone{ ID: "023e105f4ecef8ad9ca31a8372d0c353", Name: "example.com", DevMode: 7200, OriginalNS: []string{ "ns1.originaldnshost.com", "ns2.originaldnshost.com", }, OriginalRegistrar: "GoDaddy", OriginalDNSHost: "NameCheap", CreatedOn: createdAndModifiedOn, ModifiedOn: createdAndModifiedOn, Owner: Owner{ ID: "7c5dae5552338874e5053f2534d2767a", Email: "user@example.com", OwnerType: "user", }, Account: Account{ ID: "01a7362d577a6c3019a474fd6f485823", Name: "Demo Account", }, Permissions: []string{"#zone:read", "#zone:edit"}, Plan: ZonePlan{ ZonePlanCommon: ZonePlanCommon{ ID: "e592fd9519420ba7405e1307bff33214", Name: "Pro Plan", Price: 20, Currency: "USD", Frequency: "monthly", }, LegacyID: "pro", IsSubscribed: true, CanSubscribe: true, }, PlanPending: ZonePlan{ ZonePlanCommon: ZonePlanCommon{ ID: "e592fd9519420ba7405e1307bff33214", Name: "Pro Plan", Price: 20, Currency: "USD", Frequency: "monthly", }, LegacyID: "pro", IsSubscribed: true, CanSubscribe: true, }, Status: "active", Paused: false, Type: "full", NameServers: []string{"tony.ns.cloudflare.com", "woz.ns.cloudflare.com"}, } var expectedPartialZoneSetup = Zone{ ID: "023e105f4ecef8ad9ca31a8372d0c353", Name: "example.com", DevMode: 7200, OriginalNS: []string{ "ns1.originaldnshost.com", "ns2.originaldnshost.com", }, OriginalRegistrar: "GoDaddy", OriginalDNSHost: "NameCheap", CreatedOn: createdAndModifiedOn, ModifiedOn: createdAndModifiedOn, Owner: Owner{ ID: "7c5dae5552338874e5053f2534d2767a", Email: "user@example.com", OwnerType: "user", }, Account: Account{ ID: "01a7362d577a6c3019a474fd6f485823", Name: "Demo Account", }, Permissions: []string{"#zone:read", "#zone:edit"}, Plan: ZonePlan{ ZonePlanCommon: ZonePlanCommon{ ID: "e592fd9519420ba7405e1307bff33214", Name: "Pro Plan", Price: 20, Currency: "USD", Frequency: "monthly", }, LegacyID: "pro", IsSubscribed: true, CanSubscribe: true, }, PlanPending: ZonePlan{ ZonePlanCommon: ZonePlanCommon{ ID: "e592fd9519420ba7405e1307bff33214", Name: "Pro Plan", Price: 20, Currency: "USD", Frequency: "monthly", }, LegacyID: "pro", IsSubscribed: true, CanSubscribe: true, }, Status: "active", Paused: false, Type: "partial", NameServers: []string{"tony.ns.cloudflare.com", "woz.ns.cloudflare.com"}, } func TestCreateZoneFullSetup(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) w.Header().Set("content-type", "application/json") fmt.Fprintf(w, `{ "success": true, "errors": [], "messages": [], "result": { "id": "023e105f4ecef8ad9ca31a8372d0c353", "name": "example.com", "development_mode": 7200, "original_name_servers": [ "ns1.originaldnshost.com", "ns2.originaldnshost.com" ], "original_registrar": "GoDaddy", "original_dnshost": "NameCheap", "created_on": "2014-01-01T05:20:00.12345Z", "modified_on": "2014-01-01T05:20:00.12345Z", "activated_on": "2014-01-02T00:01:00.12345Z", "owner": { "id": "7c5dae5552338874e5053f2534d2767a", "email": "user@example.com", "type": "user" }, "account": { "id": "01a7362d577a6c3019a474fd6f485823", "name": "Demo Account" }, "permissions": [ "#zone:read", "#zone:edit" ], "plan": { "id": "e592fd9519420ba7405e1307bff33214", "name": "Pro Plan", "price": 20, "currency": "USD", "frequency": "monthly", "legacy_id": "pro", "is_subscribed": true, "can_subscribe": true }, "plan_pending": { "id": "e592fd9519420ba7405e1307bff33214", "name": "Pro Plan", "price": 20, "currency": "USD", "frequency": "monthly", "legacy_id": "pro", "is_subscribed": true, "can_subscribe": true }, "status": "active", "paused": false, "type": "full", "name_servers": [ "tony.ns.cloudflare.com", "woz.ns.cloudflare.com" ] } } `) } mux.HandleFunc("/zones", handler) actual, err := client.CreateZone(context.Background(), "example.com", false, Account{ID: "01a7362d577a6c3019a474fd6f485823"}, "full") if assert.NoError(t, err) { assert.Equal(t, expectedFullZoneSetup, actual) } } func TestCreateZonePartialSetup(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPost, r.Method, "Expected method 'POST', got %s", r.Method) w.Header().Set("content-type", "application/json") fmt.Fprintf(w, `{ "success": true, "errors": [], "messages": [], "result": { "id": "023e105f4ecef8ad9ca31a8372d0c353", "name": "example.com", "development_mode": 7200, "original_name_servers": [ "ns1.originaldnshost.com", "ns2.originaldnshost.com" ], "original_registrar": "GoDaddy", "original_dnshost": "NameCheap", "created_on": "2014-01-01T05:20:00.12345Z", "modified_on": "2014-01-01T05:20:00.12345Z", "activated_on": "2014-01-02T00:01:00.12345Z", "owner": { "id": "7c5dae5552338874e5053f2534d2767a", "email": "user@example.com", "type": "user" }, "account": { "id": "01a7362d577a6c3019a474fd6f485823", "name": "Demo Account" }, "permissions": [ "#zone:read", "#zone:edit" ], "plan": { "id": "e592fd9519420ba7405e1307bff33214", "name": "Pro Plan", "price": 20, "currency": "USD", "frequency": "monthly", "legacy_id": "pro", "is_subscribed": true, "can_subscribe": true }, "plan_pending": { "id": "e592fd9519420ba7405e1307bff33214", "name": "Pro Plan", "price": 20, "currency": "USD", "frequency": "monthly", "legacy_id": "pro", "is_subscribed": true, "can_subscribe": true }, "status": "active", "paused": false, "type": "partial", "name_servers": [ "tony.ns.cloudflare.com", "woz.ns.cloudflare.com" ] } } `) } mux.HandleFunc("/zones", handler) actual, err := client.CreateZone(context.Background(), "example.com", false, Account{ID: "01a7362d577a6c3019a474fd6f485823"}, "partial") if assert.NoError(t, err) { assert.Equal(t, expectedPartialZoneSetup, actual) } } func TestFallbackOrigin_FallbackOrigin(t *testing.T) { setup() defer teardown() mux.HandleFunc("/zones/foo/fallback_origin", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) w.Header().Set("content-type", "application/json") fmt.Fprintf(w, `{ "success": true, "errors": [], "messages": [], "result": { "id": "fallback_origin", "value": "app.example.com", "editable": true } }`) }) fallbackOrigin, err := client.FallbackOrigin(context.Background(), "foo") want := FallbackOrigin{ ID: "fallback_origin", Value: "app.example.com", } if assert.NoError(t, err) { assert.Equal(t, want, fallbackOrigin) } } func TestFallbackOrigin_UpdateFallbackOrigin(t *testing.T) { setup() defer teardown() mux.HandleFunc("/zones/foo/fallback_origin", func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) w.Header().Set("content-type", "application/json") w.WriteHeader(http.StatusCreated) fmt.Fprintf(w, ` { "success": true, "errors": [], "messages": [], "result": { "id": "fallback_origin", "value": "app.example.com", "editable": true } }`) }) response, err := client.UpdateFallbackOrigin(context.Background(), "foo", FallbackOrigin{Value: "app.example.com"}) want := &FallbackOriginResponse{ Result: FallbackOrigin{ ID: "fallback_origin", Value: "app.example.com", }, Response: Response{Success: true, Errors: []ResponseInfo{}, Messages: []ResponseInfo{}}, } if assert.NoError(t, err) { assert.Equal(t, want, response) } } func Test_normalizeZoneName(t *testing.T) { tests := []struct { name string zone string expected string }{ { name: "unicode stays unicode", zone: "ünì¢øðe.tld", expected: "ünì¢øðe.tld", }, { name: "valid punycode is normalized to unicode", zone: "xn--ne-7ca90ava1cya.tld", expected: "ünì¢øðe.tld", }, { name: "valid punycode in second label", zone: "example.xn--j6w193g", expected: "example.香港", }, { name: "invalid punycode is returned without change", zone: "xn-invalid.xn-invalid-tld", expected: "xn-invalid.xn-invalid-tld", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { actual := normalizeZoneName(tt.zone) assert.Equal(t, tt.expected, actual) }) } } func TestZonePartialHasVerificationKey(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#zone-zone-details (plus an undocumented field verification_key from curl to API) fmt.Fprintf(w, `{ "result": { "id": "foo", "name": "bar", "status": "active", "paused": false, "type": "partial", "development_mode": 0, "verification_key": "foo-bar", "original_name_servers": ["a","b","c","d"], "original_registrar": null, "original_dnshost": null, "modified_on": "2019-09-04T15:11:43.409805Z", "created_on": "2018-12-06T14:33:38.410126Z", "activated_on": "2018-12-06T14:34:39.274528Z", "meta": { "step": 4, "wildcard_proxiable": true, "custom_certificate_quota": 1, "page_rule_quota": 100, "phishing_detected": false, "multiple_railguns_allowed": false }, "owner": { "id": "bbbbbbbbbbbbbbbbbbbbbbbb", "type": "organization", "name": "OrgName" }, "account": { "id": "aaaaaaaaaaaaaaaaaaaaaaaa", "name": "AccountName" }, "permissions": [ "#access:edit", "#access:read", "#analytics:read", "#app:edit", "#auditlogs:read", "#billing:read", "#cache_purge:edit", "#dns_records:edit", "#dns_records:read", "#lb:edit", "#lb:read", "#legal:read", "#logs:edit", "#logs:read", "#member:read", "#organization:edit", "#organization:read", "#ssl:edit", "#ssl:read", "#stream:edit", "#stream:read", "#subscription:edit", "#subscription:read", "#waf:edit", "#waf:read", "#webhooks:edit", "#webhooks:read", "#worker:edit", "#worker:read", "#zone:edit", "#zone:read", "#zone_settings:edit", "#zone_settings:read" ], "plan": { "id": "94f3b7b768b0458b56d2cac4fe5ec0f9", "name": "Enterprise Website", "price": 0, "currency": "USD", "frequency": "monthly", "is_subscribed": true, "can_subscribe": true, "legacy_id": "enterprise", "legacy_discount": false, "externally_managed": true } }, "success": true, "errors": [], "messages": [] }`) } mux.HandleFunc("/zones/foo", handler) z, err := client.ZoneDetails(context.Background(), "foo") if assert.NoError(t, err) { assert.NotEmpty(t, z.VerificationKey) assert.Equal(t, z.VerificationKey, "foo-bar") } } func TestZoneDNSSECSetting(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#dnssec-properties fmt.Fprintf(w, `{ "result": { "status": "active", "flags": 257, "algorithm": "13", "key_type": "ECDSAP256SHA256", "digest_type": "2", "digest_algorithm": "SHA256", "digest": "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", "ds": "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", "key_tag": 42, "public_key": "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==", "modified_on": "2014-01-01T05:20:00Z" } }`) } mux.HandleFunc("/zones/foo/dnssec", handler) z, err := client.ZoneDNSSECSetting(context.Background(), "foo") if assert.NoError(t, err) { assert.Equal(t, z.Status, "active") assert.Equal(t, z.Flags, 257) assert.Equal(t, z.Algorithm, "13") assert.Equal(t, z.KeyType, "ECDSAP256SHA256") assert.Equal(t, z.DigestType, "2") assert.Equal(t, z.DigestAlgorithm, "SHA256") assert.Equal(t, z.Digest, "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") assert.Equal(t, z.DS, "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") assert.Equal(t, z.KeyTag, 42) assert.Equal(t, z.PublicKey, "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==") time, _ := time.Parse("2006-01-02T15:04:05Z", "2014-01-01T05:20:00Z") assert.Equal(t, z.ModifiedOn, time) } } func TestDeleteZoneDNSSEC(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodDelete, r.Method, "Expected method 'DELETE', got %s", r.Method) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#dnssec-properties fmt.Fprintf(w, `{ "result": "foo" }`) } mux.HandleFunc("/zones/foo/dnssec", handler) z, err := client.DeleteZoneDNSSEC(context.Background(), "foo") if assert.NoError(t, err) { assert.Equal(t, z, "foo") } } func TestUpdateZoneDNSSEC(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#dnssec-properties fmt.Fprintf(w, `{ "result": { "status": "active", "flags": 257, "algorithm": "13", "key_type": "ECDSAP256SHA256", "digest_type": "2", "digest_algorithm": "SHA256", "digest": "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", "ds": "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45", "key_tag": 42, "public_key": "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==", "modified_on": "2014-01-01T05:20:00Z" } }`) } mux.HandleFunc("/zones/foo/dnssec", handler) z, err := client.UpdateZoneDNSSEC(context.Background(), "foo", ZoneDNSSECUpdateOptions{ Status: "active", }) if assert.NoError(t, err) { assert.Equal(t, z.Status, "active") assert.Equal(t, z.Flags, 257) assert.Equal(t, z.Algorithm, "13") assert.Equal(t, z.KeyType, "ECDSAP256SHA256") assert.Equal(t, z.DigestType, "2") assert.Equal(t, z.DigestAlgorithm, "SHA256") assert.Equal(t, z.Digest, "48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") assert.Equal(t, z.DS, "example.com. 3600 IN DS 16953 13 2 48E939042E82C22542CB377B580DFDC52A361CEFDC72E7F9107E2B6BD9306A45") assert.Equal(t, z.KeyTag, 42) assert.Equal(t, z.PublicKey, "oXiGYrSTO+LSCJ3mohc8EP+CzF9KxBj8/ydXJ22pKuZP3VAC3/Md/k7xZfz470CoRyZJ6gV6vml07IC3d8xqhA==") time, _ := time.Parse("2006-01-02T15:04:05Z", "2014-01-01T05:20:00Z") assert.Equal(t, z.ModifiedOn, time) } } func TestZoneSetType(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) w.Header().Set("content-type", "application/json") fmt.Fprintf(w, `{ "result": { "type": "partial", "verification_key": "000000000-000000000", "modified_on": "2014-01-01T05:20:00Z" } }`) } mux.HandleFunc("/zones/foo", handler) z, err := client.ZoneSetType(context.Background(), "foo", "partial") if assert.NoError(t, err) { assert.Equal(t, z.Type, "partial") assert.Equal(t, z.VerificationKey, "000000000-000000000") time, _ := time.Parse("2006-01-02T15:04:05Z", "2014-01-01T05:20:00Z") assert.Equal(t, z.ModifiedOn, time) } } func parsePage(t *testing.T, total int, s string) (int, bool) { if s == "" { return 1, true } page, err := strconv.Atoi(s) if !assert.NoError(t, err) { return 0, false } if !assert.LessOrEqual(t, page, total) || !assert.GreaterOrEqual(t, page, 1) { return 0, false } return page, true } func TestListZones(t *testing.T) { setup() defer teardown() const ( total = 392 totalPage = (total + 49) / 50 ) handler := func(w http.ResponseWriter, r *http.Request) { switch { case !assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method): return case !assert.Equal(t, "50", r.URL.Query().Get("per_page")): return } page, ok := parsePage(t, totalPage, r.URL.Query().Get("page")) if !ok { return } start := (page - 1) * 50 count := 50 if page == totalPage { count = total - start } w.Header().Set("content-type", "application/json") err := json.NewEncoder(w).Encode(mockZonesResponse(total, page, start, count)) assert.NoError(t, err) } mux.HandleFunc("/zones", handler) zones, err := client.ListZones(context.Background()) if !assert.NoError(t, err) || !assert.Equal(t, total, len(zones)) { return } for i, zone := range zones { assert.Equal(t, *mockZone(i), zone) } } func TestListZonesFailingPages(t *testing.T) { setup() defer teardown() const ( total = 1489 totalPage = (total + 49) / 50 ) // the pages to reject isReject := func(i int) bool { return i == 4 || i == 7 } handler := func(w http.ResponseWriter, r *http.Request) { switch { case !assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method): return case !assert.Equal(t, "50", r.URL.Query().Get("per_page")): return } page, ok := parsePage(t, totalPage, r.URL.Query().Get("page")) switch { case !ok: return case isReject(page): return } start := (page - 1) * 50 count := 50 if page == totalPage { count = total - start } w.Header().Set("content-type", "application/json") err := json.NewEncoder(w).Encode(mockZonesResponse(total, page, start, count)) assert.NoError(t, err) } mux.HandleFunc("/zones", handler) _, err := client.ListZones(context.Background()) assert.Error(t, err) } func TestListZonesContextManualPagination1(t *testing.T) { _, err := client.ListZonesContext(context.Background(), WithPagination(PaginationOptions{Page: 2})) assert.EqualError(t, err, errManualPagination) } func TestListZonesContextManualPagination2(t *testing.T) { _, err := client.ListZonesContext(context.Background(), WithPagination(PaginationOptions{PerPage: 30})) assert.EqualError(t, err, errManualPagination) } func TestUpdateZoneSSLSettings(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) w.Header().Set("content-type", "application/json") // JSON data from: https://api.cloudflare.com/#zone-settings-properties _, _ = fmt.Fprintf(w, `{ "result": { "id": "ssl", "value": "off", "editable": true, "modified_on": "2014-01-01T05:20:00.12345Z" } }`) } mux.HandleFunc("/zones/foo/settings/ssl", handler) s, err := client.UpdateZoneSSLSettings(context.Background(), "foo", "off") if assert.NoError(t, err) { assert.Equal(t, s.ID, "ssl") assert.Equal(t, s.Value, "off") assert.Equal(t, s.Editable, true) assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") } } func TestGetZoneSetting(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) w.Header().Set("content-type", "application/json") _, _ = fmt.Fprintf(w, `{ "result": { "id": "ssl", "value": "off", "editable": true, "modified_on": "2014-01-01T05:20:00.12345Z" } }`) } mux.HandleFunc("/zones/foo/settings/ssl", handler) s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl"}) if assert.NoError(t, err) { assert.Equal(t, s.ID, "ssl") assert.Equal(t, s.Value, "off") assert.Equal(t, s.Editable, true) assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") } } func TestGetZoneSettingWithCustomPathPrefix(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodGet, r.Method, "Expected method 'GET', got %s", r.Method) w.Header().Set("content-type", "application/json") _, _ = fmt.Fprintf(w, `{ "result": { "id": "ssl", "value": "off", "editable": true, "modified_on": "2014-01-01T05:20:00.12345Z" } }`) } mux.HandleFunc("/zones/foo/my_custom_path/ssl", handler) s, err := client.GetZoneSetting(context.Background(), ZoneIdentifier("foo"), GetZoneSettingParams{Name: "ssl", PathPrefix: "my_custom_path"}) if assert.NoError(t, err) { assert.Equal(t, s.ID, "ssl") assert.Equal(t, s.Value, "off") assert.Equal(t, s.Editable, true) assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") } } func TestUpdateZoneSetting(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) w.Header().Set("content-type", "application/json") _, _ = fmt.Fprintf(w, `{ "result": { "id": "ssl", "value": "off", "editable": true, "modified_on": "2014-01-01T05:20:00.12345Z" } }`) } mux.HandleFunc("/zones/foo/settings/ssl", handler) s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{Name: "ssl", Value: "off"}) if assert.NoError(t, err) { assert.Equal(t, s.ID, "ssl") assert.Equal(t, s.Value, "off") assert.Equal(t, s.Editable, true) assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") } } func TestUpdateZoneSettingWithCustomPathPrefix(t *testing.T) { setup() defer teardown() handler := func(w http.ResponseWriter, r *http.Request) { assert.Equal(t, http.MethodPatch, r.Method, "Expected method 'PATCH', got %s", r.Method) w.Header().Set("content-type", "application/json") _, _ = fmt.Fprintf(w, `{ "result": { "id": "ssl", "value": "off", "editable": true, "modified_on": "2014-01-01T05:20:00.12345Z" } }`) } mux.HandleFunc("/zones/foo/my_custom_path/ssl", handler) s, err := client.UpdateZoneSetting(context.Background(), ZoneIdentifier("foo"), UpdateZoneSettingParams{Name: "ssl", PathPrefix: "my_custom_path", Value: "off"}) if assert.NoError(t, err) { assert.Equal(t, s.ID, "ssl") assert.Equal(t, s.Value, "off") assert.Equal(t, s.Editable, true) assert.Equal(t, s.ModifiedOn, "2014-01-01T05:20:00.12345Z") } }