From bb21b0a476b1466e133a1c4c9c7cd064a67847cf Mon Sep 17 00:00:00 2001 From: MRG FOSS Date: Fri, 8 Mar 2024 17:09:46 +0100 Subject: [PATCH] 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 --- fakestorage/object.go | 17 +++++- fakestorage/object_test.go | 111 +++++++++++++++++++++++++++++++++++++ fakestorage/response.go | 9 +++ 3 files changed, 136 insertions(+), 1 deletion(-) diff --git a/fakestorage/object.go b/fakestorage/object.go index 04f86aed5e..118d3dedbc 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/object_test.go b/fakestorage/object_test.go index e89a6647be..76873913f8 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" diff --git a/fakestorage/response.go b/fakestorage/response.go index aff80a2aaa..4e243e7bc0 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)