Repository: cloudflare/cloudflare-go
Pull request #3742 (2024-12-17 15:29:40)

Bugfix/certificate_authorities: fixes for methods to interact with Hostname Associations API
## Description

There were flaws in the initial implementation of `certificate_authorities_hostname_associations` that I discovered after manually testing the changes in #3740 on real API server:
1. Parameter mtls_certificate_id was not properly added as query param in `ListCertificateAuthoritiesHostnameAssociation` and was not validated in the test of this method.
2. Response structure was incorrectly transposed during tests, and was missing the envelope. As such, the response failed to deserialize on the real server.

## Has your change been tested?

Performed manual testing against real server
Updated Automated tests to validate both issues


## 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.
- [x] This change is using publicly documented in [cloudflare/api-schemas](https://github.com/cloudflare/api-schemas) 
      and relies on stable APIs.

[1]: https://help.github.com/articles/closing-issues-using-keywords/



Patch:
diff --git a/.changelog/3742.txt b/.changelog/3742.txt
new file mode 100644
index 000000000..0395042d0
--- /dev/null
+++ b/.changelog/3742.txt
@@ -0,0 +1,3 @@
+```release-note:bug
+certificate_authorities: fixes for methods to interact with Certificate Authorities Hostname Associations API
+```
\ No newline at end of file
diff --git a/certificate_authorities.go b/certificate_authorities.go
index b9cffb2ab..80ba39c87 100644
--- a/certificate_authorities.go
+++ b/certificate_authorities.go
@@ -17,9 +17,18 @@ type UpdateCertificateAuthoritiesHostnameAssociationsParams struct {
 	MTLSCertificateID string                `json:"mtls_certificate_id,omitempty"`
 }
 
+type HostnameAssociationsUpdateRequest struct {
+	Hostnames         []HostnameAssociation `json:"hostnames,omitempty"`
+	MTLSCertificateID string                `json:"mtls_certificate_id,omitempty"`
+}
+
 type HostnameAssociationsResponse struct {
 	Response
-	Result []HostnameAssociation `json:"result"`
+	Result HostnameAssociations `json:"result"`
+}
+
+type HostnameAssociations struct {
+	Hostnames []HostnameAssociation `json:"hostnames"`
 }
 
 type HostnameAssociation = string
@@ -28,12 +37,11 @@ type HostnameAssociation = string
 //
 // API Reference: https://developers.cloudflare.com/api/resources/certificate_authorities/subresources/hostname_associations/methods/get/
 func (api *API) ListCertificateAuthoritiesHostnameAssociations(ctx context.Context, rc *ResourceContainer, params ListCertificateAuthoritiesHostnameAssociationsParams) ([]HostnameAssociation, error) {
+	if rc.Level != ZoneRouteLevel {
+		return []HostnameAssociation{}, ErrRequiredZoneLevelResourceContainer
+	}
 
-	uri := fmt.Sprintf(
-		"/%s/%s/certificate_authorities/hostname_associations",
-		rc.Level,
-		rc.Identifier,
-	)
+	uri := buildURI(fmt.Sprintf("/zones/%s/certificate_authorities/hostname_associations", rc.Identifier), params)
 
 	res, err := api.makeRequestContext(ctx, http.MethodGet, uri, nil)
 	if err != nil {
@@ -46,18 +54,18 @@ func (api *API) ListCertificateAuthoritiesHostnameAssociations(ctx context.Conte
 		return []HostnameAssociation{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
 	}
 
-	return hostnameAssociationsResponse.Result, nil
+	return hostnameAssociationsResponse.Result.Hostnames, nil
 }
 
 // Replace Hostname Associations
 //
 // API Reference: https://developers.cloudflare.com/api/resources/certificate_authorities/subresources/hostname_associations/methods/update/
 func (api *API) UpdateCertificateAuthoritiesHostnameAssociations(ctx context.Context, rc *ResourceContainer, params UpdateCertificateAuthoritiesHostnameAssociationsParams) ([]HostnameAssociation, error) {
-	uri := fmt.Sprintf(
-		"/%s/%s/certificate_authorities/hostname_associations",
-		rc.Level,
-		rc.Identifier,
-	)
+	if rc.Level != ZoneRouteLevel {
+		return []HostnameAssociation{}, ErrRequiredZoneLevelResourceContainer
+	}
+	
+	uri := fmt.Sprintf("/zones/%s/certificate_authorities/hostname_associations", rc.Identifier)
 
 	res, err := api.makeRequestContext(ctx, http.MethodPut, uri, params)
 	if err != nil {
@@ -70,5 +78,5 @@ func (api *API) UpdateCertificateAuthoritiesHostnameAssociations(ctx context.Con
 		return []HostnameAssociation{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
 	}
 
-	return hostnameAssociationsResponse.Result, nil
+	return hostnameAssociationsResponse.Result.Hostnames, nil
 }


Test patch:
diff --git a/certificate_authorities_test.go b/certificate_authorities_test.go
index 940524a5b..cba5c57f7 100644
--- a/certificate_authorities_test.go
+++ b/certificate_authorities_test.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"testing"
 
+	"github.com/goccy/go-json"
 	"github.com/stretchr/testify/assert"
 )
 
@@ -15,15 +16,18 @@ func TestListCertificateAuthoritiesHostnameAssociations(t *testing.T) {
 
 	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, "72ef4d06-4752-4493-a60a-7421470fd585", r.URL.Query().Get("mtls_certificate_id"))
 		w.Header().Set("content-type", "application/json")
 		fmt.Fprintf(w, `{
 			"success": true,
 			"errors": [],
 			"messages": [],
-			"result": [
-				"admin.example.com",
-				"foobar.example.com"
-			]
+			"result": {
+				"hostnames": [
+					"admin.example.com",
+					"foobar.example.com"
+				]
+			}
 		}`)
 	}
 
@@ -51,19 +55,32 @@ func TestUpdateCertificateAuthoritiesHostnameAssociations(t *testing.T) {
 
 	handler := func(w http.ResponseWriter, r *http.Request) {
 		assert.Equal(t, http.MethodPut, r.Method, "Expected method 'PUT', got %s", r.Method)
+
+		wantReqHostnames := []HostnameAssociation{
+			"admin.example.com",
+			"foobar.example.com",
+		}
+		var req HostnameAssociationsUpdateRequest
+		assert.NoError(t, json.NewDecoder(r.Body).Decode(&req))
+		assert.Equal(t, "72ef4d06-4752-4493-a60a-7421470fd585", req.MTLSCertificateID)
+		assert.Equal(t, wantReqHostnames, req.Hostnames)
+
 		w.Header().Set("content-type", "application/json")
 		fmt.Fprintf(w, `{
 			"success": true,
 			"errors": [],
 			"messages": [],
-			"result": [
-				"admin.example.com",
-				"foobar.example.com"
-			]
+			"result": {
+				"hostnames": [
+					"admin.example.com",
+					"foobar.example.com"
+				]
+			}
 		}`)
 	}
 
 	hostnameAssociations := UpdateCertificateAuthoritiesHostnameAssociationsParams{
+		MTLSCertificateID: "72ef4d06-4752-4493-a60a-7421470fd585",
 		Hostnames: []HostnameAssociation{
 			"admin.example.com",
 			"foobar.example.com",