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

Add encoder/decoder for GetItem Handler #44

Merged
merged 5 commits into from
Sep 18, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.
## [Unreleased]
### Changed
- Abstract away dynamodb dependency. [#35](https://github.com/xmidt-org/argus/pull/35)
- Add unit tests for new dynamodb abstraction changes. [#39](https://github.com/xmidt-org/argus/pull/39)
- Add encoders/decoders for GetItem Handler. [#44](https://github.com/xmidt-org/argus/pull/44)


## [v0.3.5]
### Changed
Expand Down
18 changes: 17 additions & 1 deletion store/endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ import (
"crypto/sha256"
"encoding/base64"
"fmt"
"net/http"

"github.com/go-kit/kit/endpoint"
"github.com/xmidt-org/argus/model"
"net/http"
)

type KeyNotFoundError struct {
Expand Down Expand Up @@ -106,6 +107,21 @@ func NewSetEndpoint(s S) endpoint.Endpoint {
}
}

func newGetItemEndpoint(s S) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
itemRequest := request.(*getItemRequest)
itemResponse, err := s.Get(itemRequest.key)
if err != nil {
return nil, err
}
if itemRequest.owner == "" || itemRequest.owner == itemResponse.Owner {
return itemResponse, nil
}

return nil, &KeyNotFoundError{Key: itemRequest.key}
}
}

func NewGetEndpoint(s S) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
var (
Expand Down
15 changes: 15 additions & 0 deletions store/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package store

import "net/http"

type BadRequestErr struct {
Message string
}

func (bre BadRequestErr) Error() string {
return bre.Message
}

func (bre BadRequestErr) StatusCode() int {
return http.StatusBadRequest
}
9 changes: 9 additions & 0 deletions store/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@ func NewHandler(e endpoint.Endpoint, itemTTL ItemTTL) Handler {
)
}

func newGetItemHandler(s S) Handler {
return kithttp.NewServer(
newGetItemEndpoint(s),
decodeGetItemRequest,
encodeGetItemResponse,
kithttp.ServerErrorEncoder(encodeError),
)
}

type requestHandler struct {
ItemTTL ItemTTL
}
Expand Down
97 changes: 97 additions & 0 deletions store/transport.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package store

import (
"context"
"encoding/json"
"errors"
"net/http"

kithttp "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
"github.com/xmidt-org/argus/model"
)

// request URL path keys
const (
bucketVarKey = "bucket"
idVarKey = "id"
)

const (
bucketVarMissingMsg = "{bucket} URL path parameter missing"
idVarMissingMsg = "{id} URL path parameter missing"
)

// Request and Response Headers
const (
ItemOwnerHeaderKey = "X-Xmidt-Owner"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it belongs in this pr, but can we make these headers configurable? If argus is used in a context outside of Xmidt, they seem out of place.

XmidtErrorHeaderKey = "X-Xmidt-Error"
)

var ErrCastingEncodeGetItemResponse = errors.New("casting error in encodeGetItemResponse")

// TODO: since GET and DELETE are so similar, we could make them share at least the
// decoders
type getItemRequest struct {
key model.Key
owner string
}

func decodeGetItemRequest(ctx context.Context, r *http.Request) (interface{}, error) {
vars := mux.Vars(r)
bucket, ok := vars[bucketVarKey]
if !ok {
return nil, &BadRequestErr{Message: bucketVarMissingMsg}
}

id, ok := vars[idVarKey]

if !ok {
return nil, &BadRequestErr{Message: idVarMissingMsg}
}

return &getItemRequest{
key: model.Key{
Bucket: bucket,
ID: id,
},
owner: r.Header.Get(ItemOwnerHeaderKey),
}, nil
}

func encodeGetItemResponse(ctx context.Context, rw http.ResponseWriter, response interface{}) error {
item, ok := response.(*OwnableItem)
if !ok {
return ErrCastingEncodeGetItemResponse
}

if item.TTL <= 0 {
rw.WriteHeader(http.StatusNotFound)
return nil
}

data, err := json.Marshal(&item.Item)
if err != nil {
return err
}

rw.Header().Add("Content-Type", "application/json")
rw.Write(data)
return nil
}

func encodeError(ctx context.Context, err error, w http.ResponseWriter) {
w.Header().Set(XmidtErrorHeaderKey, err.Error())
if headerer, ok := err.(kithttp.Headerer); ok {
for k, values := range headerer.Headers() {
for _, v := range values {
w.Header().Add(k, v)
}
}
}
code := http.StatusInternalServerError
if sc, ok := err.(kithttp.StatusCoder); ok {
code = sc.StatusCode()
}
w.WriteHeader(code)
}
151 changes: 151 additions & 0 deletions store/transport_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package store

import (
"context"
"net/http"
"net/http/httptest"
"testing"

"github.com/gorilla/mux"
"github.com/stretchr/testify/assert"
"github.com/xmidt-org/argus/model"
)

func TestDecodeGetItemRequest(t *testing.T) {
testCases := []struct {
Name string
URLVars map[string]string
Headers map[string][]string
ExpectedDecodedRequest interface{}
ExpectedErr error
}{
{
Name: "Missing id",
URLVars: map[string]string{
"bucket": "california",
},
ExpectedErr: &BadRequestErr{Message: idVarMissingMsg},
},
{
Name: "Missing bucket",
URLVars: map[string]string{
"id": "san francisco",
},
ExpectedErr: &BadRequestErr{Message: bucketVarMissingMsg},
},
{
Name: "Happy path - No owner",
URLVars: map[string]string{
"bucket": "california",
"id": "san francisco",
},
ExpectedDecodedRequest: &getItemRequest{
key: model.Key{
Bucket: "california",
ID: "san francisco",
},
},
},
{
Name: "Happy path",
URLVars: map[string]string{
"bucket": "california",
"id": "san francisco",
},

ExpectedDecodedRequest: &getItemRequest{
key: model.Key{
Bucket: "california",
ID: "san francisco",
},
owner: "SF Giants",
},
Headers: map[string][]string{
ItemOwnerHeaderKey: []string{"SF Giants"},
},
},
}

for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
assert := assert.New(t)
r := httptest.NewRequest(http.MethodGet, "http://localhost/test", nil)
transferHeaders(testCase.Headers, r)

r = mux.SetURLVars(r, testCase.URLVars)
decodedRequest, err := decodeGetItemRequest(context.Background(), r)

assert.Equal(testCase.ExpectedDecodedRequest, decodedRequest)
assert.Equal(testCase.ExpectedErr, err)
})
}
}

func TestEncodeGetItemResponse(t *testing.T) {
testCases := []struct {
Name string
ItemResponse interface{}
ExpectedHeaders http.Header
ExpectedCode int
ExpectedBody string
ExpectedErr error
}{
{
Name: "Unexpected casting error",
ItemResponse: nil,
ExpectedHeaders: make(http.Header),
ExpectedErr: ErrCastingEncodeGetItemResponse,
// used due to limitations in httptest. In reality, any non-nil error promises nothing
// would be written to the response writer
ExpectedCode: 200,
},
{
Name: "Expired item",
ItemResponse: &OwnableItem{
Item: model.Item{
TTL: 0,
},
},
ExpectedCode: http.StatusNotFound,
ExpectedHeaders: make(http.Header),
},
{
Name: "Happy path",
ItemResponse: &OwnableItem{
Owner: "xmidt",
Item: model.Item{
TTL: 20,
Identifier: "id01",
Data: map[string]interface{}{
"key": 10,
},
},
},
ExpectedBody: `{"identifier":"id01","data":{"key":10},"ttl":20}`,
ExpectedCode: 200,
ExpectedHeaders: http.Header{
"Content-Type": []string{"application/json"},
},
},
}

for _, testCase := range testCases {
t.Run(testCase.Name, func(t *testing.T) {
assert := assert.New(t)
recorder := httptest.NewRecorder()
err := encodeGetItemResponse(context.Background(), recorder, testCase.ItemResponse)
assert.Equal(testCase.ExpectedErr, err)
assert.Equal(testCase.ExpectedBody, recorder.Body.String())
assert.Equal(testCase.ExpectedHeaders, recorder.HeaderMap)
assert.Equal(testCase.ExpectedCode, recorder.Code)
})
}
}

func transferHeaders(headers map[string][]string, r *http.Request) {
for k, values := range headers {
for _, value := range values {
r.Header.Add(k, value)
}
}
}