Repository: fsouza/fake-gcs-server
Pull request #1049 (2023-01-13 20:50:53)

Update UpdateObject function to handle more attributes
Builds off #1036

I noticed that there are currently no tests for `UpdateObject`. I would be open to adding one or a few if they are needed.

fsouza: > I noticed that there are currently no tests for `UpdateObject`. I would be open to adding one or a few if they are needed.

I think it's because the Go SDK never invokes the update endpoint, so we'd need lower level tests built on top of the HTTP client. We have that for other endpoints though, so feel free to add it 😁 
RachitSharma2001: Sorry for the late push. I have added the test `TestServerClientObjectUpdateCustomTime` in `object_test.go`. Let me know if there is anything that should be added or changed.

Patch:
diff --git a/fakestorage/object.go b/fakestorage/object.go
index 156e5ea..8613065 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -975,25 +975,45 @@ func (s *Server) patchObject(r *http.Request) jsonResponse {
 	defer backendObj.Close()
 
 	s.eventManager.Trigger(&backendObj, notification.EventMetadata, nil)
-	resp := jsonResponse{data: fromBackendObjects([]backend.StreamingObject{backendObj})[0]}
-	return resp
+	return jsonResponse{data: fromBackendObjects([]backend.StreamingObject{backendObj})[0]}
 }
 
 func (s *Server) updateObject(r *http.Request) jsonResponse {
 	vars := unescapeMuxVars(mux.Vars(r))
 	bucketName := vars["bucketName"]
 	objectName := vars["objectName"]
-	var metadata struct {
-		Metadata map[string]string `json:"metadata"`
+
+	type acls struct {
+		Entity string
+		Role   string
 	}
-	err := json.NewDecoder(r.Body).Decode(&metadata)
+
+	var payload struct {
+		Metadata   map[string]string `json:"metadata"`
+		CustomTime string
+		Acl        []acls
+	}
+	err := json.NewDecoder(r.Body).Decode(&payload)
 	if err != nil {
 		return jsonResponse{
 			status:       http.StatusBadRequest,
 			errorMessage: "Metadata in the request couldn't decode",
 		}
 	}
-	backendObj, err := s.backend.UpdateObject(bucketName, objectName, metadata.Metadata)
+
+	var attrsToUpdate backend.ObjectAttrs
+
+	attrsToUpdate.Metadata = payload.Metadata
+	attrsToUpdate.CustomTime = payload.CustomTime
+
+	if len(payload.Acl) > 0 {
+		attrsToUpdate.ACL = []storage.ACLRule{}
+		for _, aclData := range payload.Acl {
+			newAcl := storage.ACLRule{Entity: storage.ACLEntity(aclData.Entity), Role: storage.ACLRole(aclData.Role)}
+			attrsToUpdate.ACL = append(attrsToUpdate.ACL, newAcl)
+		}
+	}
+	backendObj, err := s.backend.UpdateObject(bucketName, objectName, attrsToUpdate)
 	if err != nil {
 		return jsonResponse{
 			status:       http.StatusNotFound,
diff --git a/internal/backend/fs.go b/internal/backend/fs.go
index d3fde1a..1d4268c 100644
--- a/internal/backend/fs.go
+++ b/internal/backend/fs.go
@@ -336,16 +336,17 @@ func (s *storageFS) PatchObject(bucketName, objectName string, attrsToUpdate Obj
 	return s.CreateObject(obj, NoConditions{})
 }
 
-func (s *storageFS) UpdateObject(bucketName, objectName string, metadata map[string]string) (StreamingObject, error) {
+func (s *storageFS) UpdateObject(bucketName, objectName string, attrsToUpdate ObjectAttrs) (StreamingObject, error) {
 	obj, err := s.GetObject(bucketName, objectName)
 	if err != nil {
 		return StreamingObject{}, err
 	}
 	defer obj.Close()
-	obj.Metadata = map[string]string{}
-	for k, v := range metadata {
-		obj.Metadata[k] = v
+
+	if attrsToUpdate.Metadata != nil {
+		obj.Metadata = map[string]string{}
 	}
+	obj.patch(attrsToUpdate)
 	obj.Generation = 0 // reset generation id
 	return s.CreateObject(obj, NoConditions{})
 }
diff --git a/internal/backend/memory.go b/internal/backend/memory.go
index cc8987f..fbb2215 100644
--- a/internal/backend/memory.go
+++ b/internal/backend/memory.go
@@ -310,16 +310,17 @@ func (s *storageMemory) PatchObject(bucketName, objectName string, attrsToUpdate
 	return obj, nil
 }
 
-// UpdateObject replaces an object metadata.
-func (s *storageMemory) UpdateObject(bucketName, objectName string, metadata map[string]string) (StreamingObject, error) {
+// UpdateObject replaces an object metadata, custom time, and acl.
+func (s *storageMemory) UpdateObject(bucketName, objectName string, attrsToUpdate ObjectAttrs) (StreamingObject, error) {
 	obj, err := s.GetObject(bucketName, objectName)
 	if err != nil {
 		return StreamingObject{}, err
 	}
-	obj.Metadata = map[string]string{}
-	for k, v := range metadata {
-		obj.Metadata[k] = v
+
+	if attrsToUpdate.Metadata != nil {
+		obj.Metadata = map[string]string{}
 	}
+	obj.patch(attrsToUpdate)
 	s.CreateObject(obj, NoConditions{})
 	return obj, nil
 }
diff --git a/internal/backend/storage.go b/internal/backend/storage.go
index 756dd7f..d80a07c 100644
--- a/internal/backend/storage.go
+++ b/internal/backend/storage.go
@@ -28,7 +28,7 @@ type Storage interface {
 	GetObjectWithGeneration(bucketName, objectName string, generation int64) (StreamingObject, error)
 	DeleteObject(bucketName, objectName string) error
 	PatchObject(bucketName, objectName string, attrsToUpdate ObjectAttrs) (StreamingObject, error)
-	UpdateObject(bucketName, objectName string, metadata map[string]string) (StreamingObject, error)
+	UpdateObject(bucketName, objectName string, attrsToUpdate ObjectAttrs) (StreamingObject, error)
 	ComposeObject(bucketName string, objectNames []string, destinationName string, metadata map[string]string, contentType string) (StreamingObject, error)
 }
 


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index b04a4a8..edd2c1d 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -9,6 +9,7 @@ import (
 	"compress/gzip"
 	"context"
 	"encoding/binary"
+	"encoding/json"
 	"errors"
 	"fmt"
 	"hash/crc32"
@@ -1615,6 +1616,58 @@ func checkObjectMetadata(actual, expected map[string]string, t *testing.T) {
 	}
 }
 
+func TestServerClientObjectUpdateCustomTime(t *testing.T) {
+	const (
+		bucketName  = "some-bucket"
+		objectName  = "data.txt"
+		content     = "some nice content"
+		contentType = "text/plain; charset=utf-8"
+	)
+	startTime := time.Now().Truncate(time.Second)
+	objs := []Object{
+		{
+			ObjectAttrs: ObjectAttrs{
+				BucketName:  bucketName,
+				Name:        objectName,
+				ContentType: contentType,
+				CustomTime:  startTime.Add(-5 * time.Hour),
+			},
+			Content: []byte(content),
+		},
+	}
+	url := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o/%s", bucketName, objectName)
+	runServersTest(t, runServersOptions{objs: objs}, func(t *testing.T, server *Server) {
+		client := server.HTTPClient()
+		jsonBody := []byte(`{"CustomTime": "` + formatTime(startTime) + `"}`)
+		bodyReader := bytes.NewReader(jsonBody)
+		req, err := http.NewRequest(http.MethodPut, url, bodyReader)
+		if err != nil {
+			t.Fatal(err)
+		}
+		resp, err := client.Do(req)
+		if err != nil {
+			t.Fatal(err)
+		}
+		defer resp.Body.Close()
+		if resp.StatusCode != http.StatusOK {
+			t.Errorf("wrong status returned\nwant %d\ngot  %d", http.StatusOK, resp.StatusCode)
+		}
+		data, err := io.ReadAll(resp.Body)
+		if err != nil {
+			t.Fatal(err)
+		}
+		var respJsonBody ObjectAttrs
+		err = json.Unmarshal(data, &respJsonBody)
+		if err != nil {
+			t.Fatal(err)
+		}
+		updatedCustomTime := respJsonBody.CustomTime
+		if !updatedCustomTime.Equal(startTime) {
+			t.Errorf("unexpected custom time\nwant %q\ngot  %q", startTime.String(), updatedCustomTime.String())
+		}
+	})
+}
+
 func TestServerClientObjectPatchCustomTime(t *testing.T) {
 	const (
 		bucketName  = "some-bucket"