Repository: cloudflare/cloudflare-go (go)

Issue:
UpdateAccessUsersSeats does not work because API expects an Array, and method sends a single object
### Confirmation

- [X] My issue isn't already found on the issue tracker.
- [X] I have replicated my issue using the latest version of the library and it is still present.

### cloudflare-go version

v0.86.0

### Go environment

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/home/auyer/.cache/go-build'
GOENV='/home/auyer/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/home/auyer/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/auyer/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/lib/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.21.6'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='1'
GOMOD='/home/auyer/code/cloudflare-go/go.mod'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3957979889=/tmp/go-build -gno-record-gcc-switches'

### Expected output

```
{
	"errors": [],
	"messages": [],
	"result": [
			{
		"access_seat": false,
		"created_at": "2014-01-01T05:20:00.12345Z",
		"gateway_seat": false,
		"seat_uid": "seat_id",
		"updated_at": "2014-01-01T05:20:00.12345Z"
			}
	],
	"success": true,
	"result_info": {
			"count": 1,
			"page": 1,
			"per_page": 20,
			"total_count": 2000
	}
}
```

### Actual output

```
{
  “result”: null,
  “success”: false,
  “errors”: [
  {
    “code”: 12074,
    “message”: “access.api.error.invalid_user_seat”
    }
  ],
  “messages”:...
}
```

### Code demonstrating the issue

```
userSeatRequest := cloudflare.UpdateAccessUserSeatParams{
  SeatUID: user.SeatUID, 
  AccessSeat: cloudflare.BoolPtr(false), 
  userSeatRequest.GatewaySeat: cloudflare.BoolPtr(false)
}
removeSeatResponse, err := api.UpdateAccessUserSeat(ctx, account, userSeatRequest)
fmt.Println(removeSeatResponse, err)
```

### Steps to reproduce

1 Get a valid Seat ID
2. send the request 


### References

https://developers.cloudflare.com/api/operations/zero-trust-seats-update-a-user-seat#request-body

Pull request description:
Fixes update seats with wrong body: expected array, sent single object


## Description

The expected body is an array. Thus I fixes the body in the existing method, and added a method for multiple UserSeeats.
I can also remove the new method, and make the existing one take a list.
83c77693e654a05fbeb7338cdff8da474fe38fc9 fixes #1479

## Has your change been tested?
Yes. I made requests using this, and wrote tests to cover it.

## 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.
- [x] My change requires a change to the documentation.
- [?] I have updated the documentation accordingly. (not sure if I should add docs elsewhere) 
- [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/1480.txt b/.changelog/1480.txt
new file mode 100644
index 000000000..0fdf1bfec
--- /dev/null
+++ b/.changelog/1480.txt
@@ -0,0 +1,7 @@
+```release-note:bug
+access_seats: UpdateAccessUserSeat: fix parameters not being an array when sending to the api. This caused an error when updating a user's seat
+```
+
+```release-note:enhancement
+access_seats: Add `UpdateAccessUsersSeats` with an array as input for multiple operations
+```
diff --git a/access_seats.go b/access_seats.go
index c1d6bf213..ea44eebc7 100644
--- a/access_seats.go
+++ b/access_seats.go
@@ -28,6 +28,13 @@ type UpdateAccessUserSeatParams struct {
 	GatewaySeat *bool  `json:"gateway_seat"`
 }
 
+// UpdateAccessUsersSeatsParams represents the update payload for multiple access seats.
+type UpdateAccessUsersSeatsParams []struct {
+	SeatUID     string `json:"seat_uid,omitempty"`
+	AccessSeat  *bool  `json:"access_seat"`
+	GatewaySeat *bool  `json:"gateway_seat"`
+}
+
 // AccessUserSeatResponse represents the response from the access user seat endpoints.
 type UpdateAccessUserSeatResponse struct {
 	Response
@@ -35,7 +42,7 @@ type UpdateAccessUserSeatResponse struct {
 	ResultInfo `json:"result_info"`
 }
 
-// UpdateAccessUserSeat updates a Access User Seat.
+// UpdateAccessUserSeat updates a single Access User Seat.
 //
 // API documentation: https://developers.cloudflare.com/api/operations/zero-trust-seats-update-a-user-seat
 func (api *API) UpdateAccessUserSeat(ctx context.Context, rc *ResourceContainer, params UpdateAccessUserSeatParams) ([]AccessUpdateAccessUserSeatResult, error) {
@@ -53,6 +60,42 @@ func (api *API) UpdateAccessUserSeat(ctx context.Context, rc *ResourceContainer,
 		rc.Identifier,
 	)
 
+	// this requests expects an array of params, but this method only accepts a single param
+	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, []UpdateAccessUserSeatParams{params})
+	if err != nil {
+		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf("%s: %w", errMakeRequestError, err)
+	}
+
+	var updateAccessUserSeatResponse UpdateAccessUserSeatResponse
+	err = json.Unmarshal(res, &updateAccessUserSeatResponse)
+	if err != nil {
+		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf("%s: %w", errUnmarshalError, err)
+	}
+
+	return updateAccessUserSeatResponse.Result, nil
+}
+
+// UpdateAccessUsersSeats updates many Access User Seats.
+//
+// API documentation: https://developers.cloudflare.com/api/operations/zero-trust-seats-update-a-user-seat
+func (api *API) UpdateAccessUsersSeats(ctx context.Context, rc *ResourceContainer, params UpdateAccessUsersSeatsParams) ([]AccessUpdateAccessUserSeatResult, error) {
+	if rc.Level != AccountRouteLevel {
+		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf(errInvalidResourceContainerAccess, rc.Level)
+	}
+
+	for _, param := range params {
+		if param.SeatUID == "" {
+			return []AccessUpdateAccessUserSeatResult{}, errMissingAccessSeatUID
+		}
+	}
+
+	uri := fmt.Sprintf(
+		"/%s/%s/access/seats",
+		rc.Level,
+		rc.Identifier,
+	)
+
+	// this requests expects an array of params, but this method only accepts a single param
 	res, err := api.makeRequestContext(ctx, http.MethodPatch, uri, params)
 	if err != nil {
 		return []AccessUpdateAccessUserSeatResult{}, fmt.Errorf("%s: %w", errMakeRequestError, err)


Test patch:
diff --git a/access_seats_test.go b/access_seats_test.go
index 9b52235f6..90ef47511 100644
--- a/access_seats_test.go
+++ b/access_seats_test.go
@@ -2,6 +2,7 @@ package cloudflare
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
 	"net/http"
 	"testing"
@@ -11,6 +12,7 @@ import (
 )
 
 var testAccessGroupSeatUID = "access-group-seat-uid"
+var testAccessGroupSeatUID2 = "access-group-seat-uid2"
 
 func TestUpdateAccessUserSeat_ZoneIsNotSupported(t *testing.T) {
 	setup()
@@ -28,12 +30,28 @@ func TestUpdateAccessUserSeat_MissingUID(t *testing.T) {
 	assert.EqualError(t, err, "missing required access seat UID")
 }
 
+func TestUpdateAccessUsersSeats_MissingUID(t *testing.T) {
+	setup()
+	defer teardown()
+
+	_, err := client.UpdateAccessUsersSeats(context.Background(), testAccountRC, UpdateAccessUsersSeatsParams{{GatewaySeat: BoolPtr(false), SeatUID: "seat_id"}, {SeatUID: "", AccessSeat: BoolPtr(true)}})
+	assert.EqualError(t, err, "missing required access seat UID")
+}
+
 func TestUpdateAccessUserSeat(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)
+
+		req := []UpdateAccessUserSeatParams{}
+
+		// Try to decode the request body into the struct.
+		err := json.NewDecoder(r.Body).Decode(&req)
+		assert.NoError(t, err, "Failed to decode request body into UpdateAccessUserSeatParams")
+		assert.Equal(t, len(req), 1, "Expected 1 seat to be updated, got %d", len(req))
+
 		w.Header().Set("content-type", "application/json")
 		fmt.Fprintf(w, `{
 			"errors": [],
@@ -80,3 +98,85 @@ func TestUpdateAccessUserSeat(t *testing.T) {
 		assert.Equal(t, want, actual)
 	}
 }
+
+func TestUpdateAccessUsersSeats(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)
+
+		req := []UpdateAccessUserSeatParams{}
+
+		// Try to decode the request body into the struct.
+		err := json.NewDecoder(r.Body).Decode(&req)
+		assert.NoError(t, err, "Failed to decode request body into UpdateAccessUserSeatParams")
+		assert.Equal(t, len(req), 2, "Expected 2 seat to be updated, got %d", len(req))
+
+		w.Header().Set("content-type", "application/json")
+		fmt.Fprintf(w, `{
+			"errors": [],
+			"messages": [],
+			"result": [
+			  {
+				"access_seat": false,
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"gateway_seat": false,
+				"seat_uid": "%s",
+				"updated_at": "2014-01-01T05:20:00.12345Z"
+			  },
+			  {
+				"access_seat": false,
+				"created_at": "2014-01-01T05:20:00.12345Z",
+				"gateway_seat": false,
+				"seat_uid": "%s",
+				"updated_at": "2014-01-01T05:20:00.12345Z"
+			  }
+			],
+			"success": true,
+			"result_info": {
+			  "count": 1,
+			  "page": 1,
+			  "per_page": 20,
+			  "total_count": 2000
+			}
+		  }
+		`, testAccessGroupSeatUID, testAccessGroupSeatUID2)
+	}
+
+	mux.HandleFunc("/accounts/"+testAccountID+"/access/seats", handler)
+	createdAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	updatedAt, _ := time.Parse(time.RFC3339, "2014-01-01T05:20:00.12345Z")
+	want := []AccessUpdateAccessUserSeatResult{
+		{
+			AccessSeat:  BoolPtr(false),
+			CreatedAt:   &createdAt,
+			GatewaySeat: BoolPtr(false),
+			SeatUID:     testAccessGroupSeatUID,
+			UpdatedAt:   &updatedAt,
+		},
+		{
+			AccessSeat:  BoolPtr(false),
+			CreatedAt:   &createdAt,
+			GatewaySeat: BoolPtr(false),
+			SeatUID:     testAccessGroupSeatUID2,
+			UpdatedAt:   &updatedAt,
+		},
+	}
+
+	actual, err := client.UpdateAccessUsersSeats(context.Background(), testAccountRC, UpdateAccessUsersSeatsParams{
+		{
+			SeatUID:     testAccessGroupSeatUID,
+			AccessSeat:  BoolPtr(false),
+			GatewaySeat: BoolPtr(false),
+		},
+		{
+			SeatUID:     testAccessGroupSeatUID2,
+			AccessSeat:  BoolPtr(false),
+			GatewaySeat: BoolPtr(false),
+		},
+	})
+	if assert.NoError(t, err) {
+		assert.Equal(t, want, actual)
+	}
+}