Repository: fsouza/fake-gcs-server
Pull request #1520 (2024-03-08 17:29:13)

Support the `projection` parameter in `getObject`
According to the behavior described in the [official documentation][1]. That is, ACL information should only be returned if `projection=full` is provided as GET parameter.

This behavior is not testeable using the Go GCS client, because it hardcodes the value `full` when performing the HTTP request (see [implementation][2]).

[1]: https://cloud.google.com/storage/docs/json_api/v1/objects/get#parameters
[2]: https://github.com/googleapis/google-cloud-go/blob/storage/v1.39.0/storage/http_client.go#L413


Patch:
diff --git a/fakestorage/object.go b/fakestorage/object.go
index 04f86ae..118d3de 100644
--- a/fakestorage/object.go
+++ b/fakestorage/object.go
@@ -622,6 +622,21 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) {
 	handler := jsonToHTTPHandler(func(r *http.Request) jsonResponse {
 		vars := unescapeMuxVars(mux.Vars(r))
 
+		projection := storage.ProjectionNoACL
+		if r.URL.Query().Has("projection") {
+			switch value := strings.ToLower(r.URL.Query().Get("projection")); value {
+			case "full":
+				projection = storage.ProjectionFull
+			case "noacl":
+				projection = storage.ProjectionNoACL
+			default:
+				return jsonResponse{
+					status:       http.StatusBadRequest,
+					errorMessage: fmt.Sprintf("invalid projection: %q", value),
+				}
+			}
+		}
+
 		obj, err := s.objectWithGenerationOnValidGeneration(vars["bucketName"], vars["objectName"], r.FormValue("generation"))
 		// Calling Close before checking err is okay on objects, and the object
 		// may need to be closed whether or not there's an error.
@@ -642,7 +657,7 @@ func (s *Server) getObject(w http.ResponseWriter, r *http.Request) {
 		header.Set("Accept-Ranges", "bytes")
 		return jsonResponse{
 			header: header,
-			data:   newObjectResponse(obj.ObjectAttrs, s.externalURL),
+			data:   newProjectedObjectResponse(obj.ObjectAttrs, s.externalURL, projection),
 		}
 	})
 
diff --git a/fakestorage/response.go b/fakestorage/response.go
index aff80a2..4e243e7 100644
--- a/fakestorage/response.go
+++ b/fakestorage/response.go
@@ -9,6 +9,7 @@ import (
 	"net/url"
 	"time"
 
+	"cloud.google.com/go/storage"
 	"github.com/fsouza/fake-gcs-server/internal/backend"
 )
 
@@ -124,6 +125,14 @@ type objectResponse struct {
 	Metageneration  string                 `json:"metageneration,omitempty"`
 }
 
+func newProjectedObjectResponse(obj ObjectAttrs, externalURL string, projection storage.Projection) objectResponse {
+	objResponse := newObjectResponse(obj, externalURL)
+	if projection == storage.ProjectionNoACL {
+		objResponse.ACL = nil
+	}
+	return objResponse
+}
+
 func newObjectResponse(obj ObjectAttrs, externalURL string) objectResponse {
 	acl := getAccessControlsListFromObject(obj)
 


Test patch:
diff --git a/fakestorage/object_test.go b/fakestorage/object_test.go
index e89a664..7687391 100644
--- a/fakestorage/object_test.go
+++ b/fakestorage/object_test.go
@@ -17,6 +17,7 @@ import (
 	"net/http"
 	"net/url"
 	"reflect"
+	"strings"
 	"testing"
 	"time"
 
@@ -1842,6 +1843,116 @@ func TestServerClientObjectUpdateContentType(t *testing.T) {
 	})
 }
 
+func TestServerClientObjectProjection(t *testing.T) {
+	const (
+		bucketName = "some-bucket"
+		objectName = "data.txt"
+	)
+	objs := []Object{
+		{
+			ObjectAttrs: ObjectAttrs{
+				BucketName: bucketName,
+				Name:       objectName,
+				ACL: []storage.ACLRule{
+					{Entity: "user-1", Role: "OWNER"},
+					{Entity: "user-2", Role: "READER"},
+				},
+			},
+		},
+	}
+
+	runServersTest(t, runServersOptions{objs: objs}, func(t *testing.T, server *Server) {
+		assertProjection := func(url string, wantStatusCode int, wantACL []objectAccessControl) {
+			// Perform request
+			client := server.HTTPClient()
+			req, err := http.NewRequest(http.MethodGet, url, nil)
+			if err != nil {
+				t.Fatal(err)
+			}
+			resp, err := client.Do(req)
+			if err != nil {
+				t.Fatal(err)
+			}
+			defer resp.Body.Close()
+			data, err := io.ReadAll(resp.Body)
+			if err != nil {
+				t.Fatal(err)
+			}
+
+			// Assert status code
+			if resp.StatusCode != wantStatusCode {
+				t.Errorf("wrong status returned\nwant %d\ngot  %d\nbody: %s", wantStatusCode, resp.StatusCode, data)
+			}
+
+			// Assert ACL
+			var respJsonBody objectResponse
+			err = json.Unmarshal(data, &respJsonBody)
+			if err != nil {
+				t.Fatal(err)
+			}
+			var gotACL []objectAccessControl
+			if respJsonBody.ACL != nil {
+				gotACL = make([]objectAccessControl, len(respJsonBody.ACL))
+				for i, acl := range respJsonBody.ACL {
+					gotACL[i] = *acl
+				}
+			}
+			if !reflect.DeepEqual(gotACL, wantACL) {
+				t.Errorf("unexpected ACL\nwant %+v\ngot  %+v", wantACL, gotACL)
+			}
+
+			// Assert error (if "400 Bad Request")
+			if resp.StatusCode == http.StatusBadRequest {
+				var respJsonErrorBody errorResponse
+				err = json.Unmarshal(data, &respJsonErrorBody)
+				if err != nil {
+					t.Fatal(err)
+				}
+				if respJsonErrorBody.Error.Code != http.StatusBadRequest {
+					t.Errorf("wrong error code\nwant %d\ngot  %d", http.StatusBadRequest, respJsonErrorBody.Error.Code)
+				}
+				if !strings.Contains(respJsonErrorBody.Error.Message, "invalid projection") {
+					t.Errorf("wrong error message\nwant %q\ngot  %q", ".*invalid projection.*", respJsonErrorBody.Error.Message)
+				}
+			}
+		}
+
+		url := fmt.Sprintf("https://storage.googleapis.com/storage/v1/b/%s/o/%s", bucketName, objectName)
+		fullACL := []objectAccessControl{
+			{Bucket: bucketName, Object: objectName, Entity: "user-1", Role: "OWNER"},
+			{Bucket: bucketName, Object: objectName, Entity: "user-2", Role: "READER"},
+		}
+
+		t.Run("full projection", func(t *testing.T) {
+			projectionParamValues := []string{"full", "Full", "FULL", "fUlL"}
+			for _, value := range projectionParamValues {
+				url := fmt.Sprintf("%s?projection=%s", url, value)
+				assertProjection(url, http.StatusOK, fullACL)
+			}
+		})
+
+		t.Run("noAcl projection", func(t *testing.T) {
+			projectionParamValues := []string{"noAcl", "NoAcl", "NOACL", "nOaCl"}
+			for _, value := range projectionParamValues {
+				url := fmt.Sprintf("%s?projection=%s", url, value)
+				assertProjection(url, http.StatusOK, nil)
+			}
+		})
+
+		t.Run("invalid projection", func(t *testing.T) {
+			projectParamValues := []string{"invalid", "", "ful"}
+			for _, value := range projectParamValues {
+				url := fmt.Sprintf("%s?projection=%s", url, value)
+				assertProjection(url, http.StatusBadRequest, nil)
+			}
+		})
+
+		t.Run("default projection", func(t *testing.T) {
+			assertProjection(url, http.StatusOK, nil)
+		})
+	})
+}
+
 func TestServerClientObjectPatchCustomTime(t *testing.T) {
 	const (
 		bucketName  = "some-bucket"