Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support the projection parameter in getObject #1520

Merged
merged 1 commit into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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