Skip to content

Commit

Permalink
Support the projection parameter in getObject (#1520)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
manuteleco authored Mar 18, 2024
1 parent f45018b commit c595ce6
Show file tree
Hide file tree
Showing 3 changed files with 136 additions and 1 deletion.
17 changes: 16 additions & 1 deletion fakestorage/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
}
})

Expand Down
111 changes: 111 additions & 0 deletions fakestorage/object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"net/http"
"net/url"
"reflect"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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"
Expand Down
9 changes: 9 additions & 0 deletions fakestorage/response.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"net/url"
"time"

"cloud.google.com/go/storage"
"github.com/fsouza/fake-gcs-server/internal/backend"
)

Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit c595ce6

Please sign in to comment.