Repository: cloudflare/cloudflare-go (go)

Issue:
`PerformTraceroute` should work without an explicit colocation list
`PerformTraceroute` should work without an explicit colocation list passed to it.

## Expected behavior

When not passing/passing empty `colos` to `PerformTraceroute`, the 'colos' field should be omitted from the API request, causing all colos should be queried. [docs](https://api.cloudflare.com/#diagnostics-traceroute)

## Current behavior

When not passing/passing empty `colos` to `PerformTraceroute` an empty array is sent to the API, causing an error

```
failed to perform traceroute: error from makeRequest: HTTP status 400: content "{\n  \"result\": null,\n  \"success\": false,\n  \"errors\": [\n    {\n      \"code\": 1005,\n      \"message\": \"Colo is not a valid colo\"\n    }\n  ],\n  \"messages\": []\n}\n"
```

## Possible solution

https://github.com/cloudflare/cloudflare-go/blob/master/diagnostics.go#L14

`Colos` should include `,omitempty`

## Steps to reproduce (for bugs)

1. Have an enterprise plan
2. Execute `PerformTraceroute` with colos being an empty slice
   - for example with `go run -a github.com/jacobbednarz/cf-traceroute -api-key=${CF_TOKEN} -email=${CF_EMAIL} -targets=1.1.1.1 -account-id=${CF_ACC_ID}`

## Context

`github.com/jacobbednarz/cf-traceroute` requires an explicit colo list because of this behavior. If this is fixed, that parameter can be omitted.  
Their code does not enforce the colo list but will error out as described above. With the adapted payload the API request should work just fine. (Verified via manual curl to the API)

## References

-

## Your Environment

Include as many relevant details about the environment you experienced
the bug in:

* Version used (e.g. git sha or specific tag): ` master` 
* Environment name and version (e.g. Go 1.11, MacOS 10.14.1): ` go version go1.15.2 linux/amd64` 



Pull request:
Make `DiagnosticsTracerouteConfiguration.Colos` `omitempty`
## Description

This will ensure that `PerformTraceroute` works without an explicit
colocation list passed to it as documented in the API docs.

Fixes #546

## Has your change been tested?

The previously failing condition was introduced as a test case

## Screenshots (if appropriate):

## Types of changes

What sort of change does your code introduce/modify?

- [x] Bug fix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to change)

## Checklist:

- [x] My code follows the code style of this project.
- [ ] My change requires a change to the documentation.
- [ ] I have updated the documentation accordingly.
- [x] I have added tests to cover my changes.
- [x] All new and existing tests passed.



Patch:
diff --git a/diagnostics.go b/diagnostics.go
index 164407055..25bf97038 100644
--- a/diagnostics.go
+++ b/diagnostics.go
@@ -11,7 +11,7 @@ import (
 // diagnostics traceroute requests.
 type DiagnosticsTracerouteConfiguration struct {
 	Targets []string                                  `json:"targets"`
-	Colos   []string                                  `json:"colos"`
+	Colos   []string                                  `json:"colos,omitempty"`
 	Options DiagnosticsTracerouteConfigurationOptions `json:"options,omitempty"`
 }
 
diff --git a/zone.go b/zone.go
index 4997f921c..8d51cd028 100644
--- a/zone.go
+++ b/zone.go
@@ -850,3 +850,84 @@ func (api *API) ZoneExport(zoneID string) (string, error) {
 	}
 	return string(res), nil
 }
+
+// ZoneDNSSECResponse represents the response from the Zone DNSSEC Setting
+type ZoneDNSSECResponse struct {
+	Response
+	Result ZoneDNSSEC `json:"result"`
+}
+
+// ZoneDNSSEC represents the response from the Zone DNSSEC Setting result
+type ZoneDNSSEC struct {
+	Status          string    `json:"status"`
+	Flags           int       `json:"flags"`
+	Algorithm       string    `json:"algorithm"`
+	KeyType         string    `json:"key_type"`
+	DigestType      string    `json:"digest_type"`
+	DigestAlgorithm string    `json:"digest_algorithm"`
+	Digest          string    `json:"digest"`
+	DS              string    `json:"ds"`
+	KeyTag          int       `json:"key_tag"`
+	PublicKey       string    `json:"public_key"`
+	ModifiedOn      time.Time `json:"modified_on"`
+}
+
+// ZoneDNSSECSetting returns the DNSSEC details of a zone
+//
+// API reference: https://api.cloudflare.com/#dnssec-dnssec-details
+func (api *API) ZoneDNSSECSetting(zoneID string) (ZoneDNSSEC, error) {
+	res, err := api.makeRequest("GET", "/zones/"+zoneID+"/dnssec", nil)
+	if err != nil {
+		return ZoneDNSSEC{}, errors.Wrap(err, errMakeRequestError)
+	}
+	response := ZoneDNSSECResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return ZoneDNSSEC{}, errors.Wrap(err, errUnmarshalError)
+	}
+
+	return response.Result, nil
+}
+
+// ZoneDNSSECDeleteResponse represents the response from the Zone DNSSEC Delete request
+type ZoneDNSSECDeleteResponse struct {
+	Response
+	Result string `json:"result"`
+}
+
+// DeleteZoneDNSSEC deletes DNSSEC for zone
+//
+// API reference: https://api.cloudflare.com/#dnssec-delete-dnssec-records
+func (api *API) DeleteZoneDNSSEC(zoneID string) (string, error) {
+	res, err := api.makeRequest("DELETE", "/zones/"+zoneID+"/dnssec", nil)
+	if err != nil {
+		return "", errors.Wrap(err, errMakeRequestError)
+	}
+	response := ZoneDNSSECDeleteResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return "", errors.Wrap(err, errUnmarshalError)
+	}
+	return response.Result, nil
+}
+
+// ZoneDNSSECUpdateOptions represents the options for DNSSEC update
+type ZoneDNSSECUpdateOptions struct {
+	Status string `json:"status"`
+}
+
+// UpdateZoneDNSSEC updates DNSSEC for a zone
+//
+// API reference: https://api.cloudflare.com/#dnssec-edit-dnssec-status
+func (api *API) UpdateZoneDNSSEC(zoneID string, options ZoneDNSSECUpdateOptions) (ZoneDNSSEC, error) {
+	res, err := api.makeRequest("PATCH", "/zones/"+zoneID+"/dnssec", options)
+	if err != nil {
+		return ZoneDNSSEC{}, errors.Wrap(err, errMakeRequestError)
+	}
+	response := ZoneDNSSECResponse{}
+	err = json.Unmarshal(res, &response)
+	if err != nil {
+		return ZoneDNSSEC{}, errors.Wrap(err, errUnmarshalError)
+	}
+	return response.Result, nil
+}


Test patch:
diff --git a/diagnostics_test.go b/diagnostics_test.go
index 507307a61..930da65ad 100644
--- a/diagnostics_test.go
+++ b/diagnostics_test.go
@@ -1,6 +1,7 @@
 package cloudflare
 
 import (
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"testing"
@@ -13,7 +14,12 @@ func TestDiagnosticsPerformTraceroute(t *testing.T) {
 	defer teardown()
 
 	handler := func(w http.ResponseWriter, r *http.Request) {
+		var request DiagnosticsTracerouteConfiguration
+		var err error
 		assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method)
+		err = json.NewDecoder(r.Body).Decode(&request)
+		assert.NoError(t, err)
+		assert.Equal(t, request.Colos, []string{"den01"}, "Exepected key 'colos' to be [\"den01\"], got %+v", request.Colos)
 		w.Header().Set("content-type", "application/json")
 		fmt.Fprintf(w, `
 		{
@@ -114,3 +120,115 @@ func TestDiagnosticsPerformTraceroute(t *testing.T) {
 		assert.Equal(t, want, trace)
 	}
 }
+
+func TestDiagnosticsPerformTracerouteEmptyColos(t *testing.T) {
+	setup()
+	defer teardown()
+
+	handler := func(w http.ResponseWriter, r *http.Request) {
+		var request DiagnosticsTracerouteConfiguration
+		var err error
+		assert.Equal(t, r.Method, "POST", "Expected method 'POST', got %s", r.Method)
+		err = json.NewDecoder(r.Body).Decode(&request)
+		assert.NoError(t, err)
+		assert.Nil(t, request.Colos, "Exepected key 'colos' to be nil, got %+v", request.Colos)
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `
+		{
+  "success": true,
+  "errors": [],
+  "messages": [],
+  "result": [
+    {
+      "target": "1.1.1.1",
+      "colos": [
+        {
+          "error": "",
+          "colo": {
+            "name": "den01",
+            "city": "Denver, CO, US"
+          },
+          "traceroute_time_ms": 969,
+          "target_summary": {
+            "asn": "",
+            "ip": "1.1.1.1",
+            "name": "1.1.1.1",
+            "packet_count": 3,
+            "mean_rtt_ms": 0.021,
+            "std_dev_rtt_ms": 0.011269427669584647,
+            "min_rtt_ms": 0.014,
+            "max_rtt_ms": 0.034
+          },
+          "hops": [
+            {
+              "packets_ttl": 1,
+              "packets_sent": 3,
+              "packets_lost": 0,
+              "nodes": [
+                {
+                  "asn": "AS13335",
+                  "ip": "1.1.1.1",
+                  "name": "one.one.one.one",
+                  "packet_count": 3,
+                  "mean_rtt_ms": 0.021,
+                  "std_dev_rtt_ms": 0.011269427669584647,
+                  "min_rtt_ms": 0.014,
+                  "max_rtt_ms": 0.034
+                }
+              ]
+            }
+          ]
+        }
+      ]
+    }
+  ]
+}
+		`)
+	}
+
+	mux.HandleFunc("/accounts/01a7362d577a6c3019a474fd6f485823/diagnostics/traceroute", handler)
+
+	want := []DiagnosticsTracerouteResponseResult{{
+		Target: "1.1.1.1",
+		Colos: []DiagnosticsTracerouteResponseColos{{
+			Error: "",
+			Colo: DiagnosticsTracerouteResponseColo{
+				Name: "den01", City: "Denver, CO, US",
+			},
+			TracerouteTimeMs: 969,
+			TargetSummary: DiagnosticsTracerouteResponseNodes{
+				Asn:         "",
+				IP:          "1.1.1.1",
+				Name:        "1.1.1.1",
+				PacketCount: 3,
+				MeanRttMs:   0.021,
+				StdDevRttMs: 0.011269427669584647,
+				MinRttMs:    0.014,
+				MaxRttMs:    0.034,
+			},
+			Hops: []DiagnosticsTracerouteResponseHops{{
+				PacketsTTL:  1,
+				PacketsSent: 3,
+				PacketsLost: 0,
+				Nodes: []DiagnosticsTracerouteResponseNodes{{
+					Asn:         "AS13335",
+					IP:          "1.1.1.1",
+					Name:        "one.one.one.one",
+					PacketCount: 3,
+					MeanRttMs:   0.021,
+					StdDevRttMs: 0.011269427669584647,
+					MinRttMs:    0.014,
+					MaxRttMs:    0.034,
+				}},
+			}},
+		}},
+	},
+	}
+
+	opts := DiagnosticsTracerouteConfigurationOptions{PacketsPerTTL: 1, PacketType: "imcp", MaxTTL: 1, WaitTime: 1}
+	trace, err := client.PerformTraceroute("01a7362d577a6c3019a474fd6f485823", []string{"1.1.1.1"}, []string{}, opts)
+
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, trace)
+	}
+}
diff --git a/zone_test.go b/zone_test.go
index a58ea9aeb..b66f0e15d 100644
--- a/zone_test.go
+++ b/zone_test.go
@@ -1096,3 +1096,117 @@ func TestZonePartialHasVerificationKey(t *testing.T) {
 		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, "GET", 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("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, "DELETE", 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("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, "PATCH", 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("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)
+	}
+}